mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-23 04:22:02 -06:00

- Updated OpenaiService and related classes to enhance the initialization process by ensuring the correct order of parameters. - Simplified audio message handling by consolidating transcription logic and improving error handling. - Refactored the OpenaiController to utilize the new structure, ensuring better integration with the base chatbot framework. - Enhanced logging for better traceability during audio processing and API interactions.
4963 lines
152 KiB
TypeScript
4963 lines
152 KiB
TypeScript
import { getCollectionsDto } from '@api/dto/business.dto';
|
|
import { OfferCallDto } from '@api/dto/call.dto';
|
|
import {
|
|
ArchiveChatDto,
|
|
BlockUserDto,
|
|
DeleteMessage,
|
|
getBase64FromMediaMessageDto,
|
|
LastMessage,
|
|
MarkChatUnreadDto,
|
|
NumberBusiness,
|
|
OnWhatsAppDto,
|
|
PrivacySettingDto,
|
|
ReadMessageDto,
|
|
SendPresenceDto,
|
|
UpdateMessageDto,
|
|
WhatsAppNumberDto,
|
|
} from '@api/dto/chat.dto';
|
|
import {
|
|
AcceptGroupInvite,
|
|
CreateGroupDto,
|
|
GetParticipant,
|
|
GroupDescriptionDto,
|
|
GroupInvite,
|
|
GroupJid,
|
|
GroupPictureDto,
|
|
GroupSendInvite,
|
|
GroupSubjectDto,
|
|
GroupToggleEphemeralDto,
|
|
GroupUpdateParticipantDto,
|
|
GroupUpdateSettingDto,
|
|
} from '@api/dto/group.dto';
|
|
import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
|
|
import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
|
|
import {
|
|
Button,
|
|
ContactMessage,
|
|
KeyType,
|
|
MediaMessage,
|
|
Options,
|
|
SendAudioDto,
|
|
SendButtonsDto,
|
|
SendContactDto,
|
|
SendListDto,
|
|
SendLocationDto,
|
|
SendMediaDto,
|
|
SendPollDto,
|
|
SendPtvDto,
|
|
SendReactionDto,
|
|
SendStatusDto,
|
|
SendStickerDto,
|
|
SendTextDto,
|
|
StatusMessage,
|
|
TypeButton,
|
|
} from '@api/dto/sendMessage.dto';
|
|
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
|
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
|
|
import { ProviderFiles } from '@api/provider/sessions';
|
|
import { PrismaRepository, Query } from '@api/repository/repository.service';
|
|
import { chatbotController, waMonitor } from '@api/server.module';
|
|
import { CacheService } from '@api/services/cache.service';
|
|
import { ChannelStartupService } from '@api/services/channel.service';
|
|
import { Events, MessageSubtype, TypeMediaMessage, wa } from '@api/types/wa.types';
|
|
import { CacheEngine } from '@cache/cacheengine';
|
|
import {
|
|
CacheConf,
|
|
Chatwoot,
|
|
ConfigService,
|
|
configService,
|
|
ConfigSessionPhone,
|
|
Database,
|
|
Log,
|
|
Openai,
|
|
ProviderSession,
|
|
QrCode,
|
|
S3,
|
|
} from '@config/env.config';
|
|
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions';
|
|
import ffmpegPath from '@ffmpeg-installer/ffmpeg';
|
|
import { Boom } from '@hapi/boom';
|
|
import { createId as cuid } from '@paralleldrive/cuid2';
|
|
import { Instance, Message } from '@prisma/client';
|
|
import { createJid } from '@utils/createJid';
|
|
import { makeProxyAgent } from '@utils/makeProxyAgent';
|
|
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
|
|
import { status } from '@utils/renderStatus';
|
|
import useMultiFileAuthStatePrisma from '@utils/use-multi-file-auth-state-prisma';
|
|
import { AuthStateProvider } from '@utils/use-multi-file-auth-state-provider-files';
|
|
import { useMultiFileAuthStateRedisDb } from '@utils/use-multi-file-auth-state-redis-db';
|
|
import axios from 'axios';
|
|
import makeWASocket, {
|
|
AnyMessageContent,
|
|
BufferedEventData,
|
|
BufferJSON,
|
|
CacheStore,
|
|
CatalogCollection,
|
|
Chat,
|
|
ConnectionState,
|
|
Contact,
|
|
delay,
|
|
DisconnectReason,
|
|
downloadMediaMessage,
|
|
fetchLatestBaileysVersion,
|
|
generateWAMessageFromContent,
|
|
getAggregateVotesInPollMessage,
|
|
GetCatalogOptions,
|
|
getContentType,
|
|
getDevice,
|
|
GroupMetadata,
|
|
isJidBroadcast,
|
|
isJidGroup,
|
|
isJidNewsletter,
|
|
isJidUser,
|
|
makeCacheableSignalKeyStore,
|
|
MessageUpsertType,
|
|
MessageUserReceiptUpdate,
|
|
MiscMessageGenerationOptions,
|
|
ParticipantAction,
|
|
prepareWAMessageMedia,
|
|
Product,
|
|
proto,
|
|
UserFacingSocketConfig,
|
|
WABrowserDescription,
|
|
WAMediaUpload,
|
|
WAMessage,
|
|
WAMessageUpdate,
|
|
WAPresence,
|
|
WASocket,
|
|
} from 'baileys';
|
|
import { Label } from 'baileys/lib/Types/Label';
|
|
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
|
|
import { spawn } from 'child_process';
|
|
import { isArray, isBase64, isURL } from 'class-validator';
|
|
import { randomBytes } from 'crypto';
|
|
import EventEmitter2 from 'eventemitter2';
|
|
import ffmpeg from 'fluent-ffmpeg';
|
|
import FormData from 'form-data';
|
|
import Long from 'long';
|
|
import mimeTypes from 'mime-types';
|
|
import NodeCache from 'node-cache';
|
|
import cron from 'node-cron';
|
|
import { release } from 'os';
|
|
import { join } from 'path';
|
|
import P from 'pino';
|
|
import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
|
|
import qrcodeTerminal from 'qrcode-terminal';
|
|
import sharp from 'sharp';
|
|
import { PassThrough, Readable } from 'stream';
|
|
import { v4 } from 'uuid';
|
|
|
|
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
|
|
|
|
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
|
|
|
|
// Adicione a função getVideoDuration no início do arquivo
|
|
async function getVideoDuration(input: Buffer | string | Readable): Promise<number> {
|
|
const MediaInfoFactory = (await import('mediainfo.js')).default;
|
|
const mediainfo = await MediaInfoFactory({ format: 'JSON' });
|
|
|
|
let fileSize: number;
|
|
let readChunk: (size: number, offset: number) => Promise<Buffer>;
|
|
|
|
if (Buffer.isBuffer(input)) {
|
|
fileSize = input.length;
|
|
readChunk = async (size: number, offset: number): Promise<Buffer> => {
|
|
return input.slice(offset, offset + size);
|
|
};
|
|
} else if (typeof input === 'string') {
|
|
const fs = await import('fs');
|
|
const stat = await fs.promises.stat(input);
|
|
fileSize = stat.size;
|
|
const fd = await fs.promises.open(input, 'r');
|
|
|
|
readChunk = async (size: number, offset: number): Promise<Buffer> => {
|
|
const buffer = Buffer.alloc(size);
|
|
await fd.read(buffer, 0, size, offset);
|
|
return buffer;
|
|
};
|
|
|
|
try {
|
|
const result = await mediainfo.analyzeData(() => fileSize, readChunk);
|
|
const jsonResult = JSON.parse(result);
|
|
|
|
const generalTrack = jsonResult.media.track.find((t: any) => t['@type'] === 'General');
|
|
const duration = generalTrack.Duration;
|
|
|
|
return Math.round(parseFloat(duration));
|
|
} finally {
|
|
await fd.close();
|
|
}
|
|
} else if (input instanceof Readable) {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of input) {
|
|
chunks.push(chunk);
|
|
}
|
|
const data = Buffer.concat(chunks);
|
|
fileSize = data.length;
|
|
|
|
readChunk = async (size: number, offset: number): Promise<Buffer> => {
|
|
return data.slice(offset, offset + size);
|
|
};
|
|
} else {
|
|
throw new Error('Tipo de entrada não suportado');
|
|
}
|
|
|
|
const result = await mediainfo.analyzeData(() => fileSize, readChunk);
|
|
const jsonResult = JSON.parse(result);
|
|
|
|
const generalTrack = jsonResult.media.track.find((t: any) => t['@type'] === 'General');
|
|
const duration = generalTrack.Duration;
|
|
|
|
return Math.round(parseFloat(duration));
|
|
}
|
|
|
|
export class BaileysStartupService extends ChannelStartupService {
|
|
constructor(
|
|
public readonly configService: ConfigService,
|
|
public readonly eventEmitter: EventEmitter2,
|
|
public readonly prismaRepository: PrismaRepository,
|
|
public readonly cache: CacheService,
|
|
public readonly chatwootCache: CacheService,
|
|
public readonly baileysCache: CacheService,
|
|
private readonly providerFiles: ProviderFiles,
|
|
) {
|
|
super(configService, eventEmitter, prismaRepository, chatwootCache);
|
|
this.instance.qrcode = { count: 0 };
|
|
|
|
this.authStateProvider = new AuthStateProvider(this.providerFiles);
|
|
}
|
|
|
|
private authStateProvider: AuthStateProvider;
|
|
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
|
|
private readonly userDevicesCache: CacheStore = new NodeCache({
|
|
stdTTL: 300000,
|
|
useClones: false,
|
|
});
|
|
private endSession = false;
|
|
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
|
|
|
public stateConnection: wa.StateConnection = { state: 'close' };
|
|
|
|
public phoneNumber: string;
|
|
|
|
public get connectionStatus() {
|
|
return this.stateConnection;
|
|
}
|
|
|
|
public async logoutInstance() {
|
|
await this.client?.logout('Log out instance: ' + this.instanceName);
|
|
|
|
this.client?.ws?.close();
|
|
|
|
const sessionExists = await this.prismaRepository.session.findFirst({
|
|
where: { sessionId: this.instanceId },
|
|
});
|
|
if (sessionExists) {
|
|
await this.prismaRepository.session.delete({
|
|
where: {
|
|
sessionId: this.instanceId,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
public async getProfileName() {
|
|
let profileName = this.client.user?.name ?? this.client.user?.verifiedName;
|
|
if (!profileName) {
|
|
const data = await this.prismaRepository.session.findUnique({
|
|
where: { sessionId: this.instanceId },
|
|
});
|
|
|
|
if (data) {
|
|
const creds = JSON.parse(JSON.stringify(data.creds), BufferJSON.reviver);
|
|
profileName = creds.me?.name || creds.me?.verifiedName;
|
|
}
|
|
}
|
|
|
|
return profileName;
|
|
}
|
|
|
|
public async getProfileStatus() {
|
|
const status = await this.client.fetchStatus(this.instance.wuid);
|
|
|
|
return status[0]?.status;
|
|
}
|
|
|
|
public get profilePictureUrl() {
|
|
return this.instance.profilePictureUrl;
|
|
}
|
|
|
|
public get qrCode(): wa.QrCode {
|
|
return {
|
|
pairingCode: this.instance.qrcode?.pairingCode,
|
|
code: this.instance.qrcode?.code,
|
|
base64: this.instance.qrcode?.base64,
|
|
count: this.instance.qrcode?.count,
|
|
};
|
|
}
|
|
|
|
private async connectionUpdate({ qr, connection, lastDisconnect }: Partial<ConnectionState>) {
|
|
if (qr) {
|
|
if (this.instance.qrcode.count === this.configService.get<QrCode>('QRCODE').LIMIT) {
|
|
this.sendDataWebhook(Events.QRCODE_UPDATED, {
|
|
message: 'QR code limit reached, please login again',
|
|
statusCode: DisconnectReason.badSession,
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.QRCODE_UPDATED,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{
|
|
message: 'QR code limit reached, please login again',
|
|
statusCode: DisconnectReason.badSession,
|
|
},
|
|
);
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
|
|
instance: this.instance.name,
|
|
state: 'refused',
|
|
statusReason: DisconnectReason.connectionClosed,
|
|
wuid: this.instance.wuid,
|
|
profileName: await this.getProfileName(),
|
|
profilePictureUrl: this.instance.profilePictureUrl,
|
|
});
|
|
|
|
this.endSession = true;
|
|
|
|
return this.eventEmitter.emit('no.connection', this.instance.name);
|
|
}
|
|
|
|
this.instance.qrcode.count++;
|
|
|
|
const color = this.configService.get<QrCode>('QRCODE').COLOR;
|
|
|
|
const optsQrcode: QRCodeToDataURLOptions = {
|
|
margin: 3,
|
|
scale: 4,
|
|
errorCorrectionLevel: 'H',
|
|
color: { light: '#ffffff', dark: color },
|
|
};
|
|
|
|
if (this.phoneNumber) {
|
|
await delay(1000);
|
|
this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber);
|
|
} else {
|
|
this.instance.qrcode.pairingCode = null;
|
|
}
|
|
|
|
qrcode.toDataURL(qr, optsQrcode, (error, base64) => {
|
|
if (error) {
|
|
this.logger.error('Qrcode generate failed:' + error.toString());
|
|
return;
|
|
}
|
|
|
|
this.instance.qrcode.base64 = base64;
|
|
this.instance.qrcode.code = qr;
|
|
|
|
this.sendDataWebhook(Events.QRCODE_UPDATED, {
|
|
qrcode: {
|
|
instance: this.instance.name,
|
|
pairingCode: this.instance.qrcode.pairingCode,
|
|
code: qr,
|
|
base64,
|
|
},
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.QRCODE_UPDATED,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{
|
|
qrcode: {
|
|
instance: this.instance.name,
|
|
pairingCode: this.instance.qrcode.pairingCode,
|
|
code: qr,
|
|
base64,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
qrcodeTerminal.generate(qr, { small: true }, (qrcode) =>
|
|
this.logger.log(
|
|
`\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` +
|
|
qrcode,
|
|
),
|
|
);
|
|
|
|
await this.prismaRepository.instance.update({
|
|
where: { id: this.instanceId },
|
|
data: {
|
|
connectionStatus: 'connecting',
|
|
},
|
|
});
|
|
}
|
|
|
|
if (connection) {
|
|
this.stateConnection = {
|
|
state: connection,
|
|
statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200,
|
|
};
|
|
}
|
|
|
|
if (connection === 'close') {
|
|
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
|
const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406];
|
|
const shouldReconnect = !codesToNotReconnect.includes(statusCode);
|
|
if (shouldReconnect) {
|
|
await this.connectToWhatsapp(this.phoneNumber);
|
|
} else {
|
|
this.sendDataWebhook(Events.STATUS_INSTANCE, {
|
|
instance: this.instance.name,
|
|
status: 'closed',
|
|
disconnectionAt: new Date(),
|
|
disconnectionReasonCode: statusCode,
|
|
disconnectionObject: JSON.stringify(lastDisconnect),
|
|
});
|
|
|
|
await this.prismaRepository.instance.update({
|
|
where: { id: this.instanceId },
|
|
data: {
|
|
connectionStatus: 'close',
|
|
disconnectionAt: new Date(),
|
|
disconnectionReasonCode: statusCode,
|
|
disconnectionObject: JSON.stringify(lastDisconnect),
|
|
},
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.STATUS_INSTANCE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{
|
|
instance: this.instance.name,
|
|
status: 'closed',
|
|
},
|
|
);
|
|
}
|
|
|
|
this.eventEmitter.emit('logout.instance', this.instance.name, 'inner');
|
|
this.client?.ws?.close();
|
|
this.client.end(new Error('Close connection'));
|
|
|
|
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
|
|
instance: this.instance.name,
|
|
...this.stateConnection,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (connection === 'open') {
|
|
this.instance.wuid = this.client.user.id.replace(/:\d+/, '');
|
|
try {
|
|
const profilePic = await this.profilePicture(this.instance.wuid);
|
|
this.instance.profilePictureUrl = profilePic.profilePictureUrl;
|
|
} catch (error) {
|
|
this.instance.profilePictureUrl = null;
|
|
}
|
|
const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' ');
|
|
const formattedName = this.instance.name;
|
|
this.logger.info(
|
|
`
|
|
┌──────────────────────────────┐
|
|
│ CONNECTED TO WHATSAPP │
|
|
└──────────────────────────────┘`.replace(/^ +/gm, ' '),
|
|
);
|
|
this.logger.info(
|
|
`
|
|
wuid: ${formattedWuid}
|
|
name: ${formattedName}
|
|
`,
|
|
);
|
|
|
|
await this.prismaRepository.instance.update({
|
|
where: { id: this.instanceId },
|
|
data: {
|
|
ownerJid: this.instance.wuid,
|
|
profileName: (await this.getProfileName()) as string,
|
|
profilePicUrl: this.instance.profilePictureUrl,
|
|
connectionStatus: 'open',
|
|
},
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.CONNECTION_UPDATE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{
|
|
instance: this.instance.name,
|
|
status: 'open',
|
|
},
|
|
);
|
|
this.syncChatwootLostMessages();
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
|
|
instance: this.instance.name,
|
|
wuid: this.instance.wuid,
|
|
profileName: await this.getProfileName(),
|
|
profilePictureUrl: this.instance.profilePictureUrl,
|
|
...this.stateConnection,
|
|
});
|
|
}
|
|
|
|
if (connection === 'connecting') {
|
|
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
|
|
instance: this.instance.name,
|
|
...this.stateConnection,
|
|
});
|
|
}
|
|
}
|
|
|
|
private async getMessage(key: proto.IMessageKey, full = false) {
|
|
try {
|
|
const webMessageInfo = (await this.prismaRepository.message.findMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
key: {
|
|
path: ['id'],
|
|
equals: key.id,
|
|
},
|
|
},
|
|
})) as unknown as proto.IWebMessageInfo[];
|
|
if (full) {
|
|
return webMessageInfo[0];
|
|
}
|
|
if (webMessageInfo[0].message?.pollCreationMessage) {
|
|
const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret;
|
|
|
|
if (typeof messageSecretBase64 === 'string') {
|
|
const messageSecret = Buffer.from(messageSecretBase64, 'base64');
|
|
|
|
const msg = {
|
|
messageContextInfo: {
|
|
messageSecret,
|
|
},
|
|
pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage,
|
|
};
|
|
|
|
return msg;
|
|
}
|
|
}
|
|
|
|
return webMessageInfo[0].message;
|
|
} catch (error) {
|
|
return { conversation: '' };
|
|
}
|
|
}
|
|
|
|
private async defineAuthState() {
|
|
const db = this.configService.get<Database>('DATABASE');
|
|
const cache = this.configService.get<CacheConf>('CACHE');
|
|
|
|
const provider = this.configService.get<ProviderSession>('PROVIDER');
|
|
|
|
if (provider?.ENABLED) {
|
|
return await this.authStateProvider.authStateProvider(this.instance.id);
|
|
}
|
|
|
|
if (cache?.REDIS.ENABLED && cache?.REDIS.SAVE_INSTANCES) {
|
|
this.logger.info('Redis enabled');
|
|
return await useMultiFileAuthStateRedisDb(this.instance.id, this.cache);
|
|
}
|
|
|
|
if (db.SAVE_DATA.INSTANCE) {
|
|
return await useMultiFileAuthStatePrisma(this.instance.id, this.cache);
|
|
}
|
|
}
|
|
|
|
private async createClient(number?: string): Promise<WASocket> {
|
|
this.instance.authState = await this.defineAuthState();
|
|
|
|
const session = this.configService.get<ConfigSessionPhone>('CONFIG_SESSION_PHONE');
|
|
|
|
let browserOptions = {};
|
|
|
|
if (number || this.phoneNumber) {
|
|
this.phoneNumber = number;
|
|
|
|
this.logger.info(`Phone number: ${number}`);
|
|
} else {
|
|
const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()];
|
|
browserOptions = { browser };
|
|
|
|
this.logger.info(`Browser: ${browser}`);
|
|
}
|
|
|
|
let version;
|
|
let log;
|
|
|
|
if (session.VERSION) {
|
|
version = session.VERSION.split('.');
|
|
log = `Baileys version env: ${version}`;
|
|
} else {
|
|
const baileysVersion = await fetchLatestBaileysVersion();
|
|
version = baileysVersion.version;
|
|
log = `Baileys version: ${version}`;
|
|
}
|
|
|
|
this.logger.info(log);
|
|
|
|
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
|
|
|
|
let options;
|
|
|
|
if (this.localProxy?.enabled) {
|
|
this.logger.info('Proxy enabled: ' + this.localProxy?.host);
|
|
|
|
if (this.localProxy?.host?.includes('proxyscrape')) {
|
|
try {
|
|
const response = await axios.get(this.localProxy?.host);
|
|
const text = response.data;
|
|
const proxyUrls = text.split('\r\n');
|
|
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
|
const proxyUrl = 'http://' + proxyUrls[rand];
|
|
options = {
|
|
agent: makeProxyAgent(proxyUrl),
|
|
fetchAgent: makeProxyAgent(proxyUrl),
|
|
};
|
|
} catch (error) {
|
|
this.localProxy.enabled = false;
|
|
}
|
|
} else {
|
|
options = {
|
|
agent: makeProxyAgent({
|
|
host: this.localProxy.host,
|
|
port: this.localProxy.port,
|
|
protocol: this.localProxy.protocol,
|
|
username: this.localProxy.username,
|
|
password: this.localProxy.password,
|
|
}),
|
|
fetchAgent: makeProxyAgent({
|
|
host: this.localProxy.host,
|
|
port: this.localProxy.port,
|
|
protocol: this.localProxy.protocol,
|
|
username: this.localProxy.username,
|
|
password: this.localProxy.password,
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
const socketConfig: UserFacingSocketConfig = {
|
|
...options,
|
|
version,
|
|
logger: P({ level: this.logBaileys }),
|
|
printQRInTerminal: false,
|
|
auth: {
|
|
creds: this.instance.authState.state.creds,
|
|
keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' }) as any),
|
|
},
|
|
msgRetryCounterCache: this.msgRetryCounterCache,
|
|
generateHighQualityLinkPreview: true,
|
|
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
|
|
...browserOptions,
|
|
markOnlineOnConnect: this.localSettings.alwaysOnline,
|
|
retryRequestDelayMs: 350,
|
|
maxMsgRetryCount: 4,
|
|
fireInitQueries: true,
|
|
connectTimeoutMs: 30_000,
|
|
keepAliveIntervalMs: 30_000,
|
|
qrTimeout: 45_000,
|
|
emitOwnEvents: false,
|
|
shouldIgnoreJid: (jid) => {
|
|
if (this.localSettings.syncFullHistory && isJidGroup(jid)) {
|
|
return false;
|
|
}
|
|
|
|
const isGroupJid = this.localSettings.groupsIgnore && isJidGroup(jid);
|
|
const isBroadcast = !this.localSettings.readStatus && isJidBroadcast(jid);
|
|
const isNewsletter = isJidNewsletter(jid);
|
|
|
|
return isGroupJid || isBroadcast || isNewsletter;
|
|
},
|
|
syncFullHistory: this.localSettings.syncFullHistory,
|
|
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
|
|
return this.historySyncNotification(msg);
|
|
},
|
|
cachedGroupMetadata: this.getGroupMetadataCache,
|
|
userDevicesCache: this.userDevicesCache,
|
|
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
|
patchMessageBeforeSending(message) {
|
|
if (
|
|
message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST
|
|
) {
|
|
message = JSON.parse(JSON.stringify(message));
|
|
|
|
message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
|
|
}
|
|
|
|
if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
|
|
message = JSON.parse(JSON.stringify(message));
|
|
|
|
message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
|
|
}
|
|
|
|
return message;
|
|
},
|
|
};
|
|
|
|
this.endSession = false;
|
|
|
|
this.client = makeWASocket(socketConfig);
|
|
|
|
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
|
|
useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
|
|
}
|
|
|
|
this.eventHandler();
|
|
|
|
this.client.ws.on('CB:call', (packet) => {
|
|
console.log('CB:call', packet);
|
|
const payload = {
|
|
event: 'CB:call',
|
|
packet: packet,
|
|
};
|
|
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
|
|
});
|
|
|
|
this.client.ws.on('CB:ack,class:call', (packet) => {
|
|
console.log('CB:ack,class:call', packet);
|
|
const payload = {
|
|
event: 'CB:ack,class:call',
|
|
packet: packet,
|
|
};
|
|
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
|
|
});
|
|
|
|
this.phoneNumber = number;
|
|
|
|
return this.client;
|
|
}
|
|
|
|
public async connectToWhatsapp(number?: string): Promise<WASocket> {
|
|
try {
|
|
this.loadChatwoot();
|
|
this.loadSettings();
|
|
this.loadWebhook();
|
|
this.loadProxy();
|
|
|
|
return await this.createClient(number);
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new InternalServerErrorException(error?.toString());
|
|
}
|
|
}
|
|
|
|
public async reloadConnection(): Promise<WASocket> {
|
|
try {
|
|
return await this.createClient(this.phoneNumber);
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new InternalServerErrorException(error?.toString());
|
|
}
|
|
}
|
|
|
|
private readonly chatHandle = {
|
|
'chats.upsert': async (chats: Chat[]) => {
|
|
const existingChatIds = await this.prismaRepository.chat.findMany({
|
|
where: { instanceId: this.instanceId },
|
|
select: { remoteJid: true },
|
|
});
|
|
|
|
const existingChatIdSet = new Set(existingChatIds.map((chat) => chat.remoteJid));
|
|
|
|
const chatsToInsert = chats
|
|
.filter((chat) => !existingChatIdSet?.has(chat.id))
|
|
.map((chat) => ({
|
|
remoteJid: chat.id,
|
|
instanceId: this.instanceId,
|
|
name: chat.name,
|
|
unreadMessages: chat.unreadCount !== undefined ? chat.unreadCount : 0,
|
|
}));
|
|
|
|
this.sendDataWebhook(Events.CHATS_UPSERT, chatsToInsert);
|
|
|
|
if (chatsToInsert.length > 0) {
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS)
|
|
await this.prismaRepository.chat.createMany({
|
|
data: chatsToInsert,
|
|
skipDuplicates: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
'chats.update': async (
|
|
chats: Partial<
|
|
proto.IConversation & {
|
|
lastMessageRecvTimestamp?: number;
|
|
} & {
|
|
conditional: (bufferedData: BufferedEventData) => boolean;
|
|
}
|
|
>[],
|
|
) => {
|
|
const chatsRaw = chats.map((chat) => {
|
|
return { remoteJid: chat.id, instanceId: this.instanceId };
|
|
});
|
|
|
|
this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw);
|
|
|
|
for (const chat of chats) {
|
|
await this.prismaRepository.chat.updateMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
remoteJid: chat.id,
|
|
name: chat.name,
|
|
},
|
|
data: { remoteJid: chat.id },
|
|
});
|
|
}
|
|
},
|
|
|
|
'chats.delete': async (chats: string[]) => {
|
|
chats.forEach(
|
|
async (chat) =>
|
|
await this.prismaRepository.chat.deleteMany({
|
|
where: { instanceId: this.instanceId, remoteJid: chat },
|
|
}),
|
|
);
|
|
|
|
this.sendDataWebhook(Events.CHATS_DELETE, [...chats]);
|
|
},
|
|
};
|
|
|
|
private readonly contactHandle = {
|
|
'contacts.upsert': async (contacts: Contact[]) => {
|
|
try {
|
|
const contactsRaw: any = contacts.map((contact) => ({
|
|
remoteJid: contact.id,
|
|
pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0],
|
|
profilePicUrl: null,
|
|
instanceId: this.instanceId,
|
|
}));
|
|
|
|
if (contactsRaw.length > 0) {
|
|
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw);
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS)
|
|
await this.prismaRepository.contact.createMany({
|
|
data: contactsRaw,
|
|
skipDuplicates: true,
|
|
});
|
|
|
|
const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
|
if (usersContacts) {
|
|
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
|
|
}
|
|
}
|
|
|
|
if (
|
|
this.configService.get<Chatwoot>('CHATWOOT').ENABLED &&
|
|
this.localChatwoot?.enabled &&
|
|
this.localChatwoot.importContacts &&
|
|
contactsRaw.length
|
|
) {
|
|
this.chatwootService.addHistoryContacts(
|
|
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
|
contactsRaw,
|
|
);
|
|
chatwootImport.importHistoryContacts(
|
|
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
|
this.localChatwoot,
|
|
);
|
|
}
|
|
|
|
const updatedContacts = await Promise.all(
|
|
contacts.map(async (contact) => ({
|
|
remoteJid: contact.id,
|
|
pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0],
|
|
profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl,
|
|
instanceId: this.instanceId,
|
|
})),
|
|
);
|
|
|
|
if (updatedContacts.length > 0) {
|
|
const usersContacts = updatedContacts.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
|
if (usersContacts) {
|
|
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, updatedContacts);
|
|
await Promise.all(
|
|
updatedContacts.map(async (contact) => {
|
|
const update = this.prismaRepository.contact.updateMany({
|
|
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
|
|
data: {
|
|
profilePicUrl: contact.profilePicUrl,
|
|
},
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
const instance = { instanceName: this.instance.name, instanceId: this.instance.id };
|
|
|
|
const findParticipant = await this.chatwootService.findContact(
|
|
instance,
|
|
contact.remoteJid.split('@')[0],
|
|
);
|
|
|
|
if (!findParticipant) {
|
|
return;
|
|
}
|
|
|
|
this.chatwootService.updateContact(instance, findParticipant.id, {
|
|
name: contact.pushName,
|
|
avatar_url: contact.profilePicUrl,
|
|
});
|
|
}
|
|
|
|
return update;
|
|
}),
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
this.logger.error(`Error: ${error.message}`);
|
|
}
|
|
},
|
|
|
|
'contacts.update': async (contacts: Partial<Contact>[]) => {
|
|
const contactsRaw: {
|
|
remoteJid: string;
|
|
pushName?: string;
|
|
profilePicUrl?: string;
|
|
instanceId: string;
|
|
}[] = [];
|
|
for await (const contact of contacts) {
|
|
contactsRaw.push({
|
|
remoteJid: contact.id,
|
|
pushName: contact?.name ?? contact?.verifiedName,
|
|
profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl,
|
|
instanceId: this.instanceId,
|
|
});
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw);
|
|
|
|
const updateTransactions = contactsRaw.map((contact) =>
|
|
this.prismaRepository.contact.upsert({
|
|
where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
|
|
create: contact,
|
|
update: contact,
|
|
}),
|
|
);
|
|
await this.prismaRepository.$transaction(updateTransactions);
|
|
|
|
const usersContacts = contactsRaw.filter((c) => c.remoteJid.includes('@s.whatsapp'));
|
|
if (usersContacts) {
|
|
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.remoteJid })));
|
|
}
|
|
},
|
|
};
|
|
|
|
private readonly messageHandle = {
|
|
'messaging-history.set': async ({
|
|
messages,
|
|
chats,
|
|
contacts,
|
|
isLatest,
|
|
progress,
|
|
syncType,
|
|
}: {
|
|
chats: Chat[];
|
|
contacts: Contact[];
|
|
messages: proto.IWebMessageInfo[];
|
|
isLatest?: boolean;
|
|
progress?: number;
|
|
syncType?: proto.HistorySync.HistorySyncType;
|
|
}) => {
|
|
try {
|
|
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
|
|
console.log('received on-demand history sync, messages=', messages);
|
|
}
|
|
console.log(
|
|
`recv ${chats.length} chats, ${contacts.length} contacts, ${messages.length} msgs (is latest: ${isLatest}, progress: ${progress}%), type: ${syncType}`,
|
|
);
|
|
|
|
const instance: InstanceDto = { instanceName: this.instance.name };
|
|
|
|
let timestampLimitToImport = null;
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
|
const daysLimitToImport = this.localChatwoot?.enabled ? this.localChatwoot.daysLimitImportMessages : 1000;
|
|
|
|
const date = new Date();
|
|
timestampLimitToImport = new Date(date.setDate(date.getDate() - daysLimitToImport)).getTime() / 1000;
|
|
|
|
const maxBatchTimestamp = Math.max(...messages.map((message) => message.messageTimestamp as number));
|
|
|
|
const processBatch = maxBatchTimestamp >= timestampLimitToImport;
|
|
|
|
if (!processBatch) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const contactsMap = new Map();
|
|
|
|
for (const contact of contacts) {
|
|
if (contact.id && (contact.notify || contact.name)) {
|
|
contactsMap.set(contact.id, {
|
|
name: contact.name ?? contact.notify,
|
|
jid: contact.id,
|
|
});
|
|
}
|
|
}
|
|
|
|
const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = [];
|
|
const chatsRepository = new Set(
|
|
(
|
|
await this.prismaRepository.chat.findMany({
|
|
where: { instanceId: this.instanceId },
|
|
})
|
|
).map((chat) => chat.remoteJid),
|
|
);
|
|
|
|
for (const chat of chats) {
|
|
if (chatsRepository?.has(chat.id)) {
|
|
continue;
|
|
}
|
|
|
|
chatsRaw.push({
|
|
remoteJid: chat.id,
|
|
instanceId: this.instanceId,
|
|
name: chat.name,
|
|
});
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CHATS_SET, chatsRaw);
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.HISTORIC) {
|
|
await this.prismaRepository.chat.createMany({
|
|
data: chatsRaw,
|
|
skipDuplicates: true,
|
|
});
|
|
}
|
|
|
|
const messagesRaw: any[] = [];
|
|
|
|
const messagesRepository: Set<string> = new Set(
|
|
chatwootImport.getRepositoryMessagesCache(instance) ??
|
|
(
|
|
await this.prismaRepository.message.findMany({
|
|
select: { key: true },
|
|
where: { instanceId: this.instanceId },
|
|
})
|
|
).map((message) => {
|
|
const key = message.key as {
|
|
id: string;
|
|
};
|
|
|
|
return key.id;
|
|
}),
|
|
);
|
|
|
|
if (chatwootImport.getRepositoryMessagesCache(instance) === null) {
|
|
chatwootImport.setRepositoryMessagesCache(instance, messagesRepository);
|
|
}
|
|
|
|
for (const m of messages) {
|
|
if (!m.message || !m.key || !m.messageTimestamp) {
|
|
continue;
|
|
}
|
|
|
|
if (Long.isLong(m?.messageTimestamp)) {
|
|
m.messageTimestamp = m.messageTimestamp?.toNumber();
|
|
}
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
|
if (m.messageTimestamp <= timestampLimitToImport) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (messagesRepository?.has(m.key.id)) {
|
|
continue;
|
|
}
|
|
|
|
if (!m.pushName && !m.key.fromMe) {
|
|
const participantJid = m.participant || m.key.participant || m.key.remoteJid;
|
|
if (participantJid && contactsMap.has(participantJid)) {
|
|
m.pushName = contactsMap.get(participantJid).name;
|
|
} else if (participantJid) {
|
|
m.pushName = participantJid.split('@')[0];
|
|
}
|
|
}
|
|
|
|
messagesRaw.push(this.prepareMessage(m));
|
|
}
|
|
|
|
this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]);
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.HISTORIC) {
|
|
await this.prismaRepository.message.createMany({
|
|
data: messagesRaw,
|
|
skipDuplicates: true,
|
|
});
|
|
}
|
|
|
|
if (
|
|
this.configService.get<Chatwoot>('CHATWOOT').ENABLED &&
|
|
this.localChatwoot?.enabled &&
|
|
this.localChatwoot.importMessages &&
|
|
messagesRaw.length > 0
|
|
) {
|
|
this.chatwootService.addHistoryMessages(
|
|
instance,
|
|
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
|
|
);
|
|
}
|
|
|
|
await this.contactHandle['contacts.upsert'](
|
|
contacts
|
|
.filter((c) => !!c.notify || !!c.name)
|
|
.map((c) => ({
|
|
id: c.id,
|
|
name: c.name ?? c.notify,
|
|
})),
|
|
);
|
|
|
|
contacts = undefined;
|
|
messages = undefined;
|
|
chats = undefined;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
}
|
|
},
|
|
|
|
'messages.upsert': async (
|
|
{
|
|
messages,
|
|
type,
|
|
requestId,
|
|
}: {
|
|
messages: proto.IWebMessageInfo[];
|
|
type: MessageUpsertType;
|
|
requestId?: string;
|
|
},
|
|
settings: any,
|
|
) => {
|
|
try {
|
|
for (const received of messages) {
|
|
if (received.message?.conversation || received.message?.extendedTextMessage?.text) {
|
|
const text = received.message?.conversation || received.message?.extendedTextMessage?.text;
|
|
|
|
if (text == 'requestPlaceholder' && !requestId) {
|
|
const messageId = await this.client.requestPlaceholderResend(received.key);
|
|
|
|
console.log('requested placeholder resync, id=', messageId);
|
|
} else if (requestId) {
|
|
console.log('Message received from phone, id=', requestId, received);
|
|
}
|
|
|
|
if (text == 'onDemandHistSync') {
|
|
const messageId = await this.client.fetchMessageHistory(50, received.key, received.messageTimestamp!);
|
|
console.log('requested on-demand sync, id=', messageId);
|
|
}
|
|
}
|
|
|
|
const editedMessage =
|
|
received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage;
|
|
|
|
if (received.message?.protocolMessage?.editedMessage && editedMessage) {
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
|
|
this.chatwootService.eventWhatsapp(
|
|
'messages.edit',
|
|
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
|
editedMessage,
|
|
);
|
|
|
|
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
|
|
const oldMessage = await this.getMessage(editedMessage.key, true);
|
|
if ((oldMessage as any)?.id) {
|
|
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
|
|
? editedMessage.timestampMs?.toNumber()
|
|
: (editedMessage.timestampMs as number);
|
|
|
|
await this.prismaRepository.message.update({
|
|
where: { id: (oldMessage as any).id },
|
|
data: {
|
|
message: editedMessage.editedMessage as any,
|
|
messageTimestamp: editedMessageTimestamp,
|
|
status: 'EDITED',
|
|
},
|
|
});
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: {
|
|
fromMe: editedMessage.key.fromMe,
|
|
keyId: editedMessage.key.id,
|
|
remoteJid: editedMessage.key.remoteJid,
|
|
status: 'EDITED',
|
|
instanceId: this.instanceId,
|
|
messageId: (oldMessage as any).id,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
const messageKey = `${this.instance.id}_${received.key.id}`;
|
|
const cached = await this.baileysCache.get(messageKey);
|
|
|
|
if (cached && !editedMessage) {
|
|
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
|
|
continue;
|
|
}
|
|
|
|
await this.baileysCache.set(messageKey, true, 30 * 60);
|
|
|
|
if (
|
|
(type !== 'notify' && type !== 'append') ||
|
|
received.message?.protocolMessage ||
|
|
received.message?.pollUpdateMessage ||
|
|
!received?.message
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (Long.isLong(received.messageTimestamp)) {
|
|
received.messageTimestamp = received.messageTimestamp?.toNumber();
|
|
}
|
|
|
|
if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) {
|
|
continue;
|
|
}
|
|
|
|
const existingChat = await this.prismaRepository.chat.findFirst({
|
|
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
|
|
select: { id: true, name: true },
|
|
});
|
|
|
|
if (
|
|
existingChat &&
|
|
received.pushName &&
|
|
existingChat.name !== received.pushName &&
|
|
received.pushName.trim().length > 0 &&
|
|
!received.key.fromMe &&
|
|
!received.key.remoteJid.includes('@g.us')
|
|
) {
|
|
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
|
|
try {
|
|
await this.prismaRepository.chat.update({
|
|
where: { id: existingChat.id },
|
|
data: { name: received.pushName },
|
|
});
|
|
} catch (error) {
|
|
console.log(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const messageRaw = this.prepareMessage(received);
|
|
|
|
const isMedia =
|
|
received?.message?.imageMessage ||
|
|
received?.message?.videoMessage ||
|
|
received?.message?.stickerMessage ||
|
|
received?.message?.documentMessage ||
|
|
received?.message?.documentWithCaptionMessage ||
|
|
received?.message?.ptvMessage ||
|
|
received?.message?.audioMessage;
|
|
|
|
if (this.localSettings.readMessages && received.key.id !== 'status@broadcast') {
|
|
await this.client.readMessages([received.key]);
|
|
}
|
|
|
|
if (this.localSettings.readStatus && received.key.id === 'status@broadcast') {
|
|
await this.client.readMessages([received.key]);
|
|
}
|
|
|
|
if (
|
|
this.configService.get<Chatwoot>('CHATWOOT').ENABLED &&
|
|
this.localChatwoot?.enabled &&
|
|
!received.key.id.includes('@broadcast')
|
|
) {
|
|
const chatwootSentMessage = await this.chatwootService.eventWhatsapp(
|
|
Events.MESSAGES_UPSERT,
|
|
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
|
messageRaw,
|
|
);
|
|
|
|
if (chatwootSentMessage?.id) {
|
|
messageRaw.chatwootMessageId = chatwootSentMessage.id;
|
|
messageRaw.chatwootInboxId = chatwootSentMessage.inbox_id;
|
|
messageRaw.chatwootConversationId = chatwootSentMessage.conversation_id;
|
|
}
|
|
}
|
|
|
|
if (this.configService.get<Openai>('OPENAI').ENABLED && received?.message?.audioMessage) {
|
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
},
|
|
include: {
|
|
OpenaiCreds: true,
|
|
},
|
|
});
|
|
|
|
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
|
messageRaw.message.speechToText = await this.openaiService.speechToText(received);
|
|
}
|
|
}
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
|
|
const msg = await this.prismaRepository.message.create({
|
|
data: messageRaw,
|
|
});
|
|
|
|
if (received.key.fromMe === false) {
|
|
if (msg.status === status[3]) {
|
|
this.logger.log(`Update not read messages ${received.key.remoteJid}`);
|
|
|
|
await this.updateChatUnreadMessages(received.key.remoteJid);
|
|
} else if (msg.status === status[4]) {
|
|
this.logger.log(`Update readed messages ${received.key.remoteJid} - ${msg.messageTimestamp}`);
|
|
|
|
await this.updateMessagesReadedByTimestamp(received.key.remoteJid, msg.messageTimestamp);
|
|
}
|
|
} else {
|
|
// is send message by me
|
|
this.logger.log(`Update readed messages ${received.key.remoteJid} - ${msg.messageTimestamp}`);
|
|
|
|
await this.updateMessagesReadedByTimestamp(received.key.remoteJid, msg.messageTimestamp);
|
|
}
|
|
|
|
if (isMedia) {
|
|
if (this.configService.get<S3>('S3').ENABLE) {
|
|
try {
|
|
const message: any = received;
|
|
const media = await this.getBase64FromMediaMessage(
|
|
{
|
|
message,
|
|
},
|
|
true,
|
|
);
|
|
|
|
const { buffer, mediaType, fileName, size } = media;
|
|
const mimetype = mimeTypes.lookup(fileName).toString();
|
|
const fullName = join(
|
|
`${this.instance.id}`,
|
|
received.key.remoteJid,
|
|
mediaType,
|
|
`${Date.now()}_${fileName}`,
|
|
);
|
|
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
|
|
'Content-Type': mimetype,
|
|
});
|
|
|
|
await this.prismaRepository.media.create({
|
|
data: {
|
|
messageId: msg.id,
|
|
instanceId: this.instanceId,
|
|
type: mediaType,
|
|
fileName: fullName,
|
|
mimetype,
|
|
},
|
|
});
|
|
|
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
|
|
|
messageRaw.message.mediaUrl = mediaUrl;
|
|
|
|
await this.prismaRepository.message.update({
|
|
where: { id: msg.id },
|
|
data: messageRaw,
|
|
});
|
|
} catch (error) {
|
|
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.localWebhook.enabled) {
|
|
if (isMedia && this.localWebhook.webhookBase64) {
|
|
try {
|
|
const buffer = await downloadMediaMessage(
|
|
{ key: received.key, message: received?.message },
|
|
'buffer',
|
|
{},
|
|
{
|
|
logger: P({ level: 'error' }) as any,
|
|
reuploadRequest: this.client.updateMediaMessage,
|
|
},
|
|
);
|
|
|
|
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
|
|
} catch (error) {
|
|
this.logger.error(['Error converting media to base64', error?.message]);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.logger.log(messageRaw);
|
|
|
|
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
|
|
|
|
await chatbotController.emit({
|
|
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
|
|
remoteJid: messageRaw.key.remoteJid,
|
|
msg: messageRaw,
|
|
pushName: messageRaw.pushName,
|
|
});
|
|
|
|
const contact = await this.prismaRepository.contact.findFirst({
|
|
where: { remoteJid: received.key.remoteJid, instanceId: this.instanceId },
|
|
});
|
|
|
|
const contactRaw: { remoteJid: string; pushName: string; profilePicUrl?: string; instanceId: string } = {
|
|
remoteJid: received.key.remoteJid,
|
|
pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
|
|
profilePicUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl,
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
if (contactRaw.remoteJid === 'status@broadcast') {
|
|
continue;
|
|
}
|
|
|
|
if (contact) {
|
|
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
await this.chatwootService.eventWhatsapp(
|
|
Events.CONTACTS_UPDATE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
contactRaw,
|
|
);
|
|
}
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS)
|
|
await this.prismaRepository.contact.upsert({
|
|
where: { remoteJid_instanceId: { remoteJid: contactRaw.remoteJid, instanceId: contactRaw.instanceId } },
|
|
create: contactRaw,
|
|
update: contactRaw,
|
|
});
|
|
|
|
continue;
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS)
|
|
await this.prismaRepository.contact.upsert({
|
|
where: {
|
|
remoteJid_instanceId: {
|
|
remoteJid: contactRaw.remoteJid,
|
|
instanceId: contactRaw.instanceId,
|
|
},
|
|
},
|
|
update: contactRaw,
|
|
create: contactRaw,
|
|
});
|
|
|
|
if (contactRaw.remoteJid.includes('@s.whatsapp')) {
|
|
await saveOnWhatsappCache([{ remoteJid: contactRaw.remoteJid }]);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
}
|
|
},
|
|
|
|
'messages.update': async (args: WAMessageUpdate[], settings: any) => {
|
|
this.logger.log(`Update messages ${JSON.stringify(args, undefined, 2)}`);
|
|
|
|
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
|
|
|
for await (const { key, update } of args) {
|
|
if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) {
|
|
continue;
|
|
}
|
|
|
|
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
|
|
|
|
const cached = await this.baileysCache.get(updateKey);
|
|
|
|
if (cached) {
|
|
this.logger.info(`Message duplicated ignored: ${key.id}`);
|
|
continue;
|
|
}
|
|
|
|
await this.baileysCache.set(updateKey, true, 30 * 60);
|
|
|
|
if (status[update.status] === 'READ' && key.fromMe) {
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
'messages.read',
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{ key: key },
|
|
);
|
|
}
|
|
}
|
|
|
|
if (key.remoteJid !== 'status@broadcast') {
|
|
let pollUpdates: any;
|
|
|
|
if (update.pollUpdates) {
|
|
const pollCreation = await this.getMessage(key);
|
|
|
|
if (pollCreation) {
|
|
pollUpdates = getAggregateVotesInPollMessage({
|
|
message: pollCreation as proto.IMessage,
|
|
pollUpdates: update.pollUpdates,
|
|
});
|
|
}
|
|
}
|
|
|
|
const findMessage = await this.prismaRepository.message.findFirst({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
key: {
|
|
path: ['id'],
|
|
equals: key.id,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!findMessage) {
|
|
continue;
|
|
}
|
|
|
|
if (update.message === null && update.status === undefined) {
|
|
this.sendDataWebhook(Events.MESSAGES_DELETE, key);
|
|
|
|
const message: any = {
|
|
messageId: findMessage.id,
|
|
keyId: key.id,
|
|
remoteJid: key.remoteJid,
|
|
fromMe: key.fromMe,
|
|
participant: key?.remoteJid,
|
|
status: 'DELETED',
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: message,
|
|
});
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.MESSAGES_DELETE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
{ key: key },
|
|
);
|
|
}
|
|
|
|
continue;
|
|
} else if (update.status !== undefined && status[update.status] !== findMessage.status) {
|
|
if (!key.fromMe && key.remoteJid) {
|
|
readChatToUpdate[key.remoteJid] = true;
|
|
|
|
if (status[update.status] === status[4]) {
|
|
this.logger.log(`Update as read ${key.remoteJid} - ${findMessage.messageTimestamp}`);
|
|
this.updateMessagesReadedByTimestamp(key.remoteJid, findMessage.messageTimestamp);
|
|
}
|
|
}
|
|
|
|
await this.prismaRepository.message.update({
|
|
where: { id: findMessage.id },
|
|
data: { status: status[update.status] },
|
|
});
|
|
}
|
|
|
|
const message: any = {
|
|
messageId: findMessage.id,
|
|
keyId: key.id,
|
|
remoteJid: key.remoteJid,
|
|
fromMe: key.fromMe,
|
|
participant: key?.remoteJid,
|
|
status: status[update.status],
|
|
pollUpdates,
|
|
instanceId: this.instanceId,
|
|
};
|
|
|
|
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: message,
|
|
});
|
|
|
|
const existingChat = await this.prismaRepository.chat.findFirst({
|
|
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
|
|
});
|
|
|
|
if (existingChat) {
|
|
const chatToInsert = {
|
|
remoteJid: message.remoteJid,
|
|
instanceId: this.instanceId,
|
|
unreadMessages: 0,
|
|
};
|
|
|
|
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
|
|
try {
|
|
await this.prismaRepository.chat.update({
|
|
where: {
|
|
id: existingChat.id,
|
|
},
|
|
data: chatToInsert,
|
|
});
|
|
} catch (error) {
|
|
console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await Promise.all(Object.keys(readChatToUpdate).map((remoteJid) => this.updateChatUnreadMessages(remoteJid)));
|
|
},
|
|
};
|
|
|
|
private readonly groupHandler = {
|
|
'groups.upsert': (groupMetadata: GroupMetadata[]) => {
|
|
this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata);
|
|
},
|
|
|
|
'groups.update': (groupMetadataUpdate: Partial<GroupMetadata>[]) => {
|
|
this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate);
|
|
|
|
groupMetadataUpdate.forEach((group) => {
|
|
if (isJidGroup(group.id)) {
|
|
this.updateGroupMetadataCache(group.id);
|
|
}
|
|
});
|
|
},
|
|
|
|
'group-participants.update': (participantsUpdate: {
|
|
id: string;
|
|
participants: string[];
|
|
action: ParticipantAction;
|
|
}) => {
|
|
this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate);
|
|
|
|
this.updateGroupMetadataCache(participantsUpdate.id);
|
|
},
|
|
};
|
|
|
|
private readonly labelHandle = {
|
|
[Events.LABELS_EDIT]: async (label: Label) => {
|
|
this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name });
|
|
|
|
const labelsRepository = await this.prismaRepository.label.findMany({
|
|
where: { instanceId: this.instanceId },
|
|
});
|
|
|
|
const savedLabel = labelsRepository.find((l) => l.labelId === label.id);
|
|
if (label.deleted && savedLabel) {
|
|
await this.prismaRepository.label.delete({
|
|
where: { labelId_instanceId: { instanceId: this.instanceId, labelId: label.id } },
|
|
});
|
|
this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name });
|
|
return;
|
|
}
|
|
|
|
const labelName = label.name.replace(/[^\x20-\x7E]/g, '');
|
|
if (!savedLabel || savedLabel.color !== `${label.color}` || savedLabel.name !== labelName) {
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.LABELS) {
|
|
const labelData = {
|
|
color: `${label.color}`,
|
|
name: labelName,
|
|
labelId: label.id,
|
|
predefinedId: label.predefinedId,
|
|
instanceId: this.instanceId,
|
|
};
|
|
await this.prismaRepository.label.upsert({
|
|
where: {
|
|
labelId_instanceId: {
|
|
instanceId: labelData.instanceId,
|
|
labelId: labelData.labelId,
|
|
},
|
|
},
|
|
update: labelData,
|
|
create: labelData,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
[Events.LABELS_ASSOCIATION]: async (
|
|
data: { association: LabelAssociation; type: 'remove' | 'add' },
|
|
database: Database,
|
|
) => {
|
|
this.logger.info(
|
|
`labels association - ${data?.association?.chatId} (${data.type}-${data?.association?.type}): ${data?.association?.labelId}`,
|
|
);
|
|
if (database.SAVE_DATA.CHATS) {
|
|
const instanceId = this.instanceId;
|
|
const chatId = data.association.chatId;
|
|
const labelId = data.association.labelId;
|
|
|
|
if (data.type === 'add') {
|
|
await this.addLabel(labelId, instanceId, chatId);
|
|
} else if (data.type === 'remove') {
|
|
await this.removeLabel(labelId, instanceId, chatId);
|
|
}
|
|
}
|
|
|
|
this.sendDataWebhook(Events.LABELS_ASSOCIATION, {
|
|
instance: this.instance.name,
|
|
type: data.type,
|
|
chatId: data.association.chatId,
|
|
labelId: data.association.labelId,
|
|
});
|
|
},
|
|
};
|
|
|
|
private eventHandler() {
|
|
this.client.ev.process(async (events) => {
|
|
if (!this.endSession) {
|
|
const database = this.configService.get<Database>('DATABASE');
|
|
const settings = await this.findSettings();
|
|
|
|
if (events.call) {
|
|
const call = events.call[0];
|
|
|
|
if (settings?.rejectCall && call.status == 'offer') {
|
|
this.client.rejectCall(call.id, call.from);
|
|
}
|
|
|
|
if (settings?.msgCall?.trim().length > 0 && call.status == 'offer') {
|
|
const msg = await this.client.sendMessage(call.from, {
|
|
text: settings.msgCall,
|
|
});
|
|
|
|
this.client.ev.emit('messages.upsert', {
|
|
messages: [msg],
|
|
type: 'notify',
|
|
});
|
|
}
|
|
|
|
this.sendDataWebhook(Events.CALL, call);
|
|
}
|
|
|
|
if (events['connection.update']) {
|
|
this.connectionUpdate(events['connection.update']);
|
|
}
|
|
|
|
if (events['creds.update']) {
|
|
this.instance.authState.saveCreds();
|
|
}
|
|
|
|
if (events['messaging-history.set']) {
|
|
const payload = events['messaging-history.set'];
|
|
this.messageHandle['messaging-history.set'](payload);
|
|
}
|
|
|
|
if (events['messages.upsert']) {
|
|
const payload = events['messages.upsert'];
|
|
this.messageHandle['messages.upsert'](payload, settings);
|
|
}
|
|
|
|
if (events['messages.update']) {
|
|
const payload = events['messages.update'];
|
|
this.messageHandle['messages.update'](payload, settings);
|
|
}
|
|
|
|
if (events['message-receipt.update']) {
|
|
const payload = events['message-receipt.update'] as MessageUserReceiptUpdate[];
|
|
const remotesJidMap: Record<string, number> = {};
|
|
|
|
for (const event of payload) {
|
|
if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') {
|
|
remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp;
|
|
}
|
|
}
|
|
|
|
await Promise.all(
|
|
Object.keys(remotesJidMap).map(async (remoteJid) =>
|
|
this.updateMessagesReadedByTimestamp(remoteJid, remotesJidMap[remoteJid]),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (events['presence.update']) {
|
|
const payload = events['presence.update'];
|
|
|
|
if (settings?.groupsIgnore && payload.id.includes('@g.us')) {
|
|
return;
|
|
}
|
|
|
|
this.sendDataWebhook(Events.PRESENCE_UPDATE, payload);
|
|
}
|
|
|
|
if (!settings?.groupsIgnore) {
|
|
if (events['groups.upsert']) {
|
|
const payload = events['groups.upsert'];
|
|
this.groupHandler['groups.upsert'](payload);
|
|
}
|
|
|
|
if (events['groups.update']) {
|
|
const payload = events['groups.update'];
|
|
this.groupHandler['groups.update'](payload);
|
|
}
|
|
|
|
if (events['group-participants.update']) {
|
|
const payload = events['group-participants.update'];
|
|
this.groupHandler['group-participants.update'](payload);
|
|
}
|
|
}
|
|
|
|
if (events['chats.upsert']) {
|
|
const payload = events['chats.upsert'];
|
|
this.chatHandle['chats.upsert'](payload);
|
|
}
|
|
|
|
if (events['chats.update']) {
|
|
const payload = events['chats.update'];
|
|
this.chatHandle['chats.update'](payload);
|
|
}
|
|
|
|
if (events['chats.delete']) {
|
|
const payload = events['chats.delete'];
|
|
this.chatHandle['chats.delete'](payload);
|
|
}
|
|
|
|
if (events['contacts.upsert']) {
|
|
const payload = events['contacts.upsert'];
|
|
this.contactHandle['contacts.upsert'](payload);
|
|
}
|
|
|
|
if (events['contacts.update']) {
|
|
const payload = events['contacts.update'];
|
|
this.contactHandle['contacts.update'](payload);
|
|
}
|
|
|
|
if (events[Events.LABELS_ASSOCIATION]) {
|
|
const payload = events[Events.LABELS_ASSOCIATION];
|
|
this.labelHandle[Events.LABELS_ASSOCIATION](payload, database);
|
|
return;
|
|
}
|
|
|
|
if (events[Events.LABELS_EDIT]) {
|
|
const payload = events[Events.LABELS_EDIT];
|
|
this.labelHandle[Events.LABELS_EDIT](payload);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private historySyncNotification(msg: proto.Message.IHistorySyncNotification) {
|
|
const instance: InstanceDto = { instanceName: this.instance.name };
|
|
|
|
if (
|
|
this.configService.get<Chatwoot>('CHATWOOT').ENABLED &&
|
|
this.localChatwoot?.enabled &&
|
|
this.localChatwoot.importMessages &&
|
|
this.isSyncNotificationFromUsedSyncType(msg)
|
|
) {
|
|
if (msg.chunkOrder === 1) {
|
|
this.chatwootService.startImportHistoryMessages(instance);
|
|
}
|
|
|
|
if (msg.progress === 100) {
|
|
setTimeout(() => {
|
|
this.chatwootService.importHistoryMessages(instance);
|
|
}, 10000);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private isSyncNotificationFromUsedSyncType(msg: proto.Message.IHistorySyncNotification) {
|
|
return (
|
|
(this.localSettings.syncFullHistory && msg?.syncType === 2) ||
|
|
(!this.localSettings.syncFullHistory && msg?.syncType === 3)
|
|
);
|
|
}
|
|
|
|
public async profilePicture(number: string) {
|
|
const jid = createJid(number);
|
|
|
|
try {
|
|
const profilePictureUrl = await this.client.profilePictureUrl(jid, 'image');
|
|
|
|
return {
|
|
wuid: jid,
|
|
profilePictureUrl,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
wuid: jid,
|
|
profilePictureUrl: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
public async getStatus(number: string) {
|
|
const jid = createJid(number);
|
|
|
|
try {
|
|
return {
|
|
wuid: jid,
|
|
status: (await this.client.fetchStatus(jid))[0]?.status,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
wuid: jid,
|
|
status: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
public async fetchProfile(instanceName: string, number?: string) {
|
|
const jid = number ? createJid(number) : this.client?.user?.id;
|
|
|
|
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
|
|
|
if (!onWhatsapp.exists) {
|
|
throw new BadRequestException(onWhatsapp);
|
|
}
|
|
|
|
try {
|
|
if (number) {
|
|
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
|
const picture = await this.profilePicture(info?.jid);
|
|
const status = await this.getStatus(info?.jid);
|
|
const business = await this.fetchBusinessProfile(info?.jid);
|
|
|
|
return {
|
|
wuid: info?.jid || jid,
|
|
name: info?.name,
|
|
numberExists: info?.exists,
|
|
picture: picture?.profilePictureUrl,
|
|
status: status?.status,
|
|
isBusiness: business.isBusiness,
|
|
email: business?.email,
|
|
description: business?.description,
|
|
website: business?.website?.shift(),
|
|
};
|
|
} else {
|
|
const instanceNames = instanceName ? [instanceName] : null;
|
|
const info: Instance = await waMonitor.instanceInfo(instanceNames);
|
|
const business = await this.fetchBusinessProfile(jid);
|
|
|
|
return {
|
|
wuid: jid,
|
|
name: info?.profileName,
|
|
numberExists: true,
|
|
picture: info?.profilePicUrl,
|
|
status: info?.connectionStatus,
|
|
isBusiness: business.isBusiness,
|
|
email: business?.email,
|
|
description: business?.description,
|
|
website: business?.website?.shift(),
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
wuid: jid,
|
|
name: null,
|
|
picture: null,
|
|
status: null,
|
|
os: null,
|
|
isBusiness: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
public async offerCall({ number, isVideo, callDuration }: OfferCallDto) {
|
|
const jid = createJid(number);
|
|
|
|
try {
|
|
const call = await this.client.offerCall(jid, isVideo);
|
|
setTimeout(() => this.client.terminateCall(call.id, call.to), callDuration * 1000);
|
|
|
|
return call;
|
|
} catch (error) {
|
|
return error;
|
|
}
|
|
}
|
|
|
|
private async sendMessage(
|
|
sender: string,
|
|
message: any,
|
|
mentions: any,
|
|
linkPreview: any,
|
|
quoted: any,
|
|
messageId?: string,
|
|
ephemeralExpiration?: number,
|
|
// participants?: GroupParticipant[],
|
|
) {
|
|
sender = sender.toLowerCase();
|
|
|
|
const option: any = {
|
|
quoted,
|
|
};
|
|
|
|
if (isJidGroup(sender)) {
|
|
option.useCachedGroupMetadata = true;
|
|
// if (participants)
|
|
// option.cachedGroupMetadata = async () => {
|
|
// return { participants: participants as GroupParticipant[] };
|
|
// };
|
|
}
|
|
|
|
if (ephemeralExpiration) option.ephemeralExpiration = ephemeralExpiration;
|
|
|
|
if (messageId) option.messageId = messageId;
|
|
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
|
|
|
|
if (message['viewOnceMessage']) {
|
|
const m = generateWAMessageFromContent(sender, message, {
|
|
timestamp: new Date(),
|
|
userJid: this.instance.wuid,
|
|
messageId,
|
|
quoted,
|
|
});
|
|
const id = await this.client.relayMessage(sender, message, { messageId });
|
|
m.key = {
|
|
id: id,
|
|
remoteJid: sender,
|
|
participant: isJidUser(sender) ? sender : undefined,
|
|
fromMe: true,
|
|
};
|
|
for (const [key, value] of Object.entries(m)) {
|
|
if (!value || (isArray(value) && value.length) === 0) {
|
|
delete m[key];
|
|
}
|
|
}
|
|
return m;
|
|
}
|
|
|
|
if (
|
|
!message['audio'] &&
|
|
!message['poll'] &&
|
|
!message['sticker'] &&
|
|
!message['conversation'] &&
|
|
sender !== 'status@broadcast'
|
|
) {
|
|
if (message['reactionMessage']) {
|
|
return await this.client.sendMessage(
|
|
sender,
|
|
{
|
|
react: {
|
|
text: message['reactionMessage']['text'],
|
|
key: message['reactionMessage']['key'],
|
|
},
|
|
} as unknown as AnyMessageContent,
|
|
option as unknown as MiscMessageGenerationOptions,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (message['conversation']) {
|
|
return await this.client.sendMessage(
|
|
sender,
|
|
{
|
|
text: message['conversation'],
|
|
mentions,
|
|
linkPreview: linkPreview,
|
|
} as unknown as AnyMessageContent,
|
|
option as unknown as MiscMessageGenerationOptions,
|
|
);
|
|
}
|
|
|
|
if (!message['audio'] && !message['poll'] && !message['sticker'] && sender != 'status@broadcast') {
|
|
return await this.client.sendMessage(
|
|
sender,
|
|
{
|
|
forward: {
|
|
key: { remoteJid: this.instance.wuid, fromMe: true },
|
|
message,
|
|
},
|
|
mentions,
|
|
},
|
|
option as unknown as MiscMessageGenerationOptions,
|
|
);
|
|
}
|
|
|
|
if (sender === 'status@broadcast') {
|
|
let jidList;
|
|
if (message['status'].option.allContacts) {
|
|
const contacts = await this.prismaRepository.contact.findMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
remoteJid: {
|
|
not: {
|
|
endsWith: '@g.us',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
jidList = contacts.map((contact) => contact.remoteJid);
|
|
} else {
|
|
jidList = message['status'].option.statusJidList;
|
|
}
|
|
|
|
const batchSize = 10;
|
|
|
|
const batches = Array.from({ length: Math.ceil(jidList.length / batchSize) }, (_, i) =>
|
|
jidList.slice(i * batchSize, i * batchSize + batchSize),
|
|
);
|
|
|
|
let msgId: string | null = null;
|
|
|
|
let firstMessage: WAMessage;
|
|
|
|
const firstBatch = batches.shift();
|
|
|
|
if (firstBatch) {
|
|
firstMessage = await this.client.sendMessage(
|
|
sender,
|
|
message['status'].content as unknown as AnyMessageContent,
|
|
{
|
|
backgroundColor: message['status'].option.backgroundColor,
|
|
font: message['status'].option.font,
|
|
statusJidList: firstBatch,
|
|
} as unknown as MiscMessageGenerationOptions,
|
|
);
|
|
|
|
msgId = firstMessage.key.id;
|
|
}
|
|
|
|
if (batches.length === 0) return firstMessage;
|
|
|
|
await Promise.allSettled(
|
|
batches.map(async (batch) => {
|
|
const messageSent = await this.client.sendMessage(
|
|
sender,
|
|
message['status'].content as unknown as AnyMessageContent,
|
|
{
|
|
backgroundColor: message['status'].option.backgroundColor,
|
|
font: message['status'].option.font,
|
|
statusJidList: batch,
|
|
messageId: msgId,
|
|
} as unknown as MiscMessageGenerationOptions,
|
|
);
|
|
|
|
return messageSent;
|
|
}),
|
|
);
|
|
|
|
return firstMessage;
|
|
}
|
|
|
|
return await this.client.sendMessage(
|
|
sender,
|
|
message as unknown as AnyMessageContent,
|
|
option as unknown as MiscMessageGenerationOptions,
|
|
);
|
|
}
|
|
|
|
private async sendMessageWithTyping<T = proto.IMessage>(
|
|
number: string,
|
|
message: T,
|
|
options?: Options,
|
|
isIntegration = false,
|
|
) {
|
|
const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift();
|
|
|
|
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
|
|
throw new BadRequestException(isWA);
|
|
}
|
|
|
|
const sender = isWA.jid.toLowerCase();
|
|
|
|
this.logger.verbose(`Sending message to ${sender}`);
|
|
|
|
try {
|
|
if (options?.delay) {
|
|
this.logger.verbose(`Typing for ${options.delay}ms to ${sender}`);
|
|
if (options.delay > 20000) {
|
|
let remainingDelay = options.delay;
|
|
while (remainingDelay > 20000) {
|
|
await this.client.presenceSubscribe(sender);
|
|
|
|
await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender);
|
|
|
|
await delay(20000);
|
|
|
|
await this.client.sendPresenceUpdate('paused', sender);
|
|
|
|
remainingDelay -= 20000;
|
|
}
|
|
if (remainingDelay > 0) {
|
|
await this.client.presenceSubscribe(sender);
|
|
|
|
await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender);
|
|
|
|
await delay(remainingDelay);
|
|
|
|
await this.client.sendPresenceUpdate('paused', sender);
|
|
}
|
|
} else {
|
|
await this.client.presenceSubscribe(sender);
|
|
|
|
await this.client.sendPresenceUpdate((options.presence as WAPresence) ?? 'composing', sender);
|
|
|
|
await delay(options.delay);
|
|
|
|
await this.client.sendPresenceUpdate('paused', sender);
|
|
}
|
|
}
|
|
|
|
const linkPreview = options?.linkPreview != false ? undefined : false;
|
|
|
|
let quoted: WAMessage;
|
|
|
|
if (options?.quoted) {
|
|
const m = options?.quoted;
|
|
|
|
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo);
|
|
|
|
if (msg) {
|
|
quoted = msg;
|
|
}
|
|
}
|
|
|
|
let messageSent: WAMessage;
|
|
|
|
let mentions: string[];
|
|
if (isJidGroup(sender)) {
|
|
let group;
|
|
try {
|
|
const cache = this.configService.get<CacheConf>('CACHE');
|
|
if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner');
|
|
else group = await this.getGroupMetadataCache(sender);
|
|
// group = await this.findGroup({ groupJid: sender }, 'inner');
|
|
} catch (error) {
|
|
throw new NotFoundException('Group not found');
|
|
}
|
|
|
|
if (!group) {
|
|
throw new NotFoundException('Group not found');
|
|
}
|
|
|
|
if (options?.mentionsEveryOne) {
|
|
mentions = group.participants.map((participant) => participant.id);
|
|
} else if (options?.mentioned?.length) {
|
|
mentions = options.mentioned.map((mention) => {
|
|
const jid = createJid(mention);
|
|
if (isJidGroup(jid)) {
|
|
return null;
|
|
}
|
|
return jid;
|
|
});
|
|
}
|
|
|
|
messageSent = await this.sendMessage(
|
|
sender,
|
|
message,
|
|
mentions,
|
|
linkPreview,
|
|
quoted,
|
|
null,
|
|
group?.ephemeralDuration,
|
|
// group?.participants,
|
|
);
|
|
} else {
|
|
messageSent = await this.sendMessage(sender, message, mentions, linkPreview, quoted);
|
|
}
|
|
|
|
if (Long.isLong(messageSent?.messageTimestamp)) {
|
|
messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber();
|
|
}
|
|
|
|
const messageRaw = this.prepareMessage(messageSent);
|
|
|
|
const isMedia =
|
|
messageSent?.message?.imageMessage ||
|
|
messageSent?.message?.videoMessage ||
|
|
messageSent?.message?.stickerMessage ||
|
|
messageSent?.message?.ptvMessage ||
|
|
messageSent?.message?.documentMessage ||
|
|
messageSent?.message?.documentWithCaptionMessage ||
|
|
messageSent?.message?.ptvMessage ||
|
|
messageSent?.message?.audioMessage;
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && !isIntegration) {
|
|
this.chatwootService.eventWhatsapp(
|
|
Events.SEND_MESSAGE,
|
|
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
|
messageRaw,
|
|
);
|
|
}
|
|
|
|
if (this.configService.get<Openai>('OPENAI').ENABLED && messageRaw?.message?.audioMessage) {
|
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
},
|
|
include: {
|
|
OpenaiCreds: true,
|
|
},
|
|
});
|
|
|
|
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
|
messageRaw.message.speechToText = await this.openaiService.speechToText(messageRaw);
|
|
}
|
|
}
|
|
|
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
|
|
const msg = await this.prismaRepository.message.create({
|
|
data: messageRaw,
|
|
});
|
|
|
|
if (isMedia && this.configService.get<S3>('S3').ENABLE) {
|
|
try {
|
|
const message: any = messageRaw;
|
|
const media = await this.getBase64FromMediaMessage(
|
|
{
|
|
message,
|
|
},
|
|
true,
|
|
);
|
|
|
|
const { buffer, mediaType, fileName, size } = media;
|
|
|
|
const mimetype = mimeTypes.lookup(fileName).toString();
|
|
|
|
const fullName = join(
|
|
`${this.instance.id}`,
|
|
messageRaw.key.remoteJid,
|
|
`${messageRaw.key.id}`,
|
|
mediaType,
|
|
fileName,
|
|
);
|
|
|
|
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
|
|
'Content-Type': mimetype,
|
|
});
|
|
|
|
await this.prismaRepository.media.create({
|
|
data: {
|
|
messageId: msg.id,
|
|
instanceId: this.instanceId,
|
|
type: mediaType,
|
|
fileName: fullName,
|
|
mimetype,
|
|
},
|
|
});
|
|
|
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
|
|
|
messageRaw.message.mediaUrl = mediaUrl;
|
|
|
|
await this.prismaRepository.message.update({
|
|
where: { id: msg.id },
|
|
data: messageRaw,
|
|
});
|
|
} catch (error) {
|
|
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.localWebhook.enabled) {
|
|
if (isMedia && this.localWebhook.webhookBase64) {
|
|
try {
|
|
const buffer = await downloadMediaMessage(
|
|
{ key: messageRaw.key, message: messageRaw?.message },
|
|
'buffer',
|
|
{},
|
|
{
|
|
logger: P({ level: 'error' }) as any,
|
|
reuploadRequest: this.client.updateMediaMessage,
|
|
},
|
|
);
|
|
|
|
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
|
|
} catch (error) {
|
|
this.logger.error(['Error converting media to base64', error?.message]);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.logger.log(messageRaw);
|
|
|
|
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
|
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled && isIntegration) {
|
|
await chatbotController.emit({
|
|
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
|
|
remoteJid: messageRaw.key.remoteJid,
|
|
msg: messageRaw,
|
|
pushName: messageRaw.pushName,
|
|
isIntegration,
|
|
});
|
|
}
|
|
|
|
return messageRaw;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
// Instance Controller
|
|
public async sendPresence(data: SendPresenceDto) {
|
|
try {
|
|
const { number } = data;
|
|
|
|
const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift();
|
|
|
|
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
|
|
throw new BadRequestException(isWA);
|
|
}
|
|
|
|
const sender = isWA.jid;
|
|
|
|
if (data?.delay && data?.delay > 20000) {
|
|
let remainingDelay = data?.delay;
|
|
while (remainingDelay > 20000) {
|
|
await this.client.presenceSubscribe(sender);
|
|
|
|
await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender);
|
|
|
|
await delay(20000);
|
|
|
|
await this.client.sendPresenceUpdate('paused', sender);
|
|
|
|
remainingDelay -= 20000;
|
|
}
|
|
if (remainingDelay > 0) {
|
|
await this.client.presenceSubscribe(sender);
|
|
|
|
await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender);
|
|
|
|
await delay(remainingDelay);
|
|
|
|
await this.client.sendPresenceUpdate('paused', sender);
|
|
}
|
|
} else {
|
|
await this.client.presenceSubscribe(sender);
|
|
|
|
await this.client.sendPresenceUpdate((data?.presence as WAPresence) ?? 'composing', sender);
|
|
|
|
await delay(data?.delay);
|
|
|
|
await this.client.sendPresenceUpdate('paused', sender);
|
|
}
|
|
|
|
return { presence: data.presence };
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
// Presence Controller
|
|
public async setPresence(data: SetPresenceDto) {
|
|
try {
|
|
await this.client.sendPresenceUpdate(data.presence);
|
|
|
|
return { presence: data.presence };
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
// Send Message Controller
|
|
public async textMessage(data: SendTextDto, isIntegration = false) {
|
|
const text = data.text;
|
|
|
|
if (!text || text.trim().length === 0) {
|
|
throw new BadRequestException('Text is required');
|
|
}
|
|
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
conversation: data.text,
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
isIntegration,
|
|
);
|
|
}
|
|
|
|
public async pollMessage(data: SendPollDto) {
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
poll: {
|
|
name: data.name,
|
|
selectableCount: data.selectableCount,
|
|
values: data.values,
|
|
},
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
linkPreview: data?.linkPreview,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
}
|
|
|
|
private async formatStatusMessage(status: StatusMessage) {
|
|
if (!status.type) {
|
|
throw new BadRequestException('Type is required');
|
|
}
|
|
|
|
if (!status.content) {
|
|
throw new BadRequestException('Content is required');
|
|
}
|
|
|
|
if (status.allContacts) {
|
|
const contacts = await this.prismaRepository.contact.findMany({
|
|
where: { instanceId: this.instanceId },
|
|
});
|
|
|
|
if (!contacts.length) {
|
|
throw new BadRequestException('Contacts not found');
|
|
}
|
|
|
|
status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.remoteJid);
|
|
}
|
|
|
|
if (!status.statusJidList?.length && !status.allContacts) {
|
|
throw new BadRequestException('StatusJidList is required');
|
|
}
|
|
|
|
if (status.type === 'text') {
|
|
if (!status.backgroundColor) {
|
|
throw new BadRequestException('Background color is required');
|
|
}
|
|
|
|
if (!status.font) {
|
|
throw new BadRequestException('Font is required');
|
|
}
|
|
|
|
return {
|
|
content: {
|
|
text: status.content,
|
|
},
|
|
option: {
|
|
backgroundColor: status.backgroundColor,
|
|
font: status.font,
|
|
statusJidList: status.statusJidList,
|
|
},
|
|
};
|
|
}
|
|
if (status.type === 'image') {
|
|
return {
|
|
content: {
|
|
image: {
|
|
url: status.content,
|
|
},
|
|
caption: status.caption,
|
|
},
|
|
option: {
|
|
statusJidList: status.statusJidList,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (status.type === 'video') {
|
|
return {
|
|
content: {
|
|
video: {
|
|
url: status.content,
|
|
},
|
|
caption: status.caption,
|
|
},
|
|
option: {
|
|
statusJidList: status.statusJidList,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (status.type === 'audio') {
|
|
const convert = await this.processAudioMp4(status.content);
|
|
if (Buffer.isBuffer(convert)) {
|
|
const result = {
|
|
content: {
|
|
audio: convert,
|
|
ptt: true,
|
|
mimetype: 'audio/ogg; codecs=opus',
|
|
},
|
|
option: {
|
|
statusJidList: status.statusJidList,
|
|
},
|
|
};
|
|
|
|
return result;
|
|
} else {
|
|
throw new InternalServerErrorException(convert);
|
|
}
|
|
}
|
|
|
|
throw new BadRequestException('Type not found');
|
|
}
|
|
|
|
public async statusMessage(data: SendStatusDto, file?: any) {
|
|
const mediaData: SendStatusDto = { ...data };
|
|
|
|
if (file) mediaData.content = file.buffer.toString('base64');
|
|
|
|
const status = await this.formatStatusMessage(mediaData);
|
|
|
|
const statusSent = await this.sendMessageWithTyping('status@broadcast', {
|
|
status,
|
|
});
|
|
|
|
return statusSent;
|
|
}
|
|
|
|
private async prepareMediaMessage(mediaMessage: MediaMessage) {
|
|
try {
|
|
const type = mediaMessage.mediatype === 'ptv' ? 'video' : mediaMessage.mediatype;
|
|
|
|
const prepareMedia = await prepareWAMessageMedia(
|
|
{
|
|
[type]: isURL(mediaMessage.media) ? { url: mediaMessage.media } : Buffer.from(mediaMessage.media, 'base64'),
|
|
} as any,
|
|
{ upload: this.client.waUploadToServer },
|
|
);
|
|
|
|
const mediaType = mediaMessage.mediatype + 'Message';
|
|
|
|
if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) {
|
|
const regex = new RegExp(/.*\/(.+?)\./);
|
|
const arrayMatch = regex.exec(mediaMessage.media);
|
|
mediaMessage.fileName = arrayMatch[1];
|
|
}
|
|
|
|
if (mediaMessage.mediatype === 'image' && !mediaMessage.fileName) {
|
|
mediaMessage.fileName = 'image.png';
|
|
}
|
|
|
|
if (mediaMessage.mediatype === 'video' && !mediaMessage.fileName) {
|
|
mediaMessage.fileName = 'video.mp4';
|
|
}
|
|
|
|
let mimetype: string | false;
|
|
|
|
if (mediaMessage.mimetype) {
|
|
mimetype = mediaMessage.mimetype;
|
|
} else {
|
|
mimetype = mimeTypes.lookup(mediaMessage.fileName);
|
|
|
|
if (!mimetype && isURL(mediaMessage.media)) {
|
|
let config: any = {
|
|
responseType: 'arraybuffer',
|
|
};
|
|
|
|
if (this.localProxy?.enabled) {
|
|
config = {
|
|
...config,
|
|
httpsAgent: makeProxyAgent({
|
|
host: this.localProxy.host,
|
|
port: this.localProxy.port,
|
|
protocol: this.localProxy.protocol,
|
|
username: this.localProxy.username,
|
|
password: this.localProxy.password,
|
|
}),
|
|
};
|
|
}
|
|
|
|
const response = await axios.get(mediaMessage.media, config);
|
|
|
|
mimetype = response.headers['content-type'];
|
|
}
|
|
}
|
|
|
|
if (mediaMessage.mediatype === 'ptv') {
|
|
prepareMedia[mediaType] = prepareMedia[type + 'Message'];
|
|
mimetype = 'video/mp4';
|
|
|
|
if (!prepareMedia[mediaType]) {
|
|
throw new Error('Failed to prepare video message');
|
|
}
|
|
|
|
try {
|
|
let mediaInput;
|
|
if (isURL(mediaMessage.media)) {
|
|
mediaInput = mediaMessage.media;
|
|
} else {
|
|
const mediaBuffer = Buffer.from(mediaMessage.media, 'base64');
|
|
if (!mediaBuffer || mediaBuffer.length === 0) {
|
|
throw new Error('Invalid media buffer');
|
|
}
|
|
mediaInput = mediaBuffer;
|
|
}
|
|
|
|
const duration = await getVideoDuration(mediaInput);
|
|
if (!duration || duration <= 0) {
|
|
throw new Error('Invalid media duration');
|
|
}
|
|
|
|
this.logger.verbose(`Video duration: ${duration} seconds`);
|
|
prepareMedia[mediaType].seconds = duration;
|
|
} catch (error) {
|
|
this.logger.error('Error getting video duration:');
|
|
this.logger.error(error);
|
|
throw new Error(`Failed to get video duration: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
prepareMedia[mediaType].caption = mediaMessage?.caption;
|
|
prepareMedia[mediaType].mimetype = mimetype;
|
|
prepareMedia[mediaType].fileName = mediaMessage.fileName;
|
|
|
|
if (mediaMessage.mediatype === 'video') {
|
|
prepareMedia[mediaType].gifPlayback = false;
|
|
}
|
|
|
|
return generateWAMessageFromContent(
|
|
'',
|
|
{ [mediaType]: { ...prepareMedia[mediaType] } },
|
|
{ userJid: this.instance.wuid },
|
|
);
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new InternalServerErrorException(error?.toString() || error);
|
|
}
|
|
}
|
|
|
|
private async convertToWebP(image: string): Promise<Buffer> {
|
|
try {
|
|
let imageBuffer: Buffer;
|
|
|
|
if (isBase64(image)) {
|
|
const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, '');
|
|
imageBuffer = Buffer.from(base64Data, 'base64');
|
|
} else {
|
|
const timestamp = new Date().getTime();
|
|
const url = `${image}?timestamp=${timestamp}`;
|
|
|
|
let config: any = {
|
|
responseType: 'arraybuffer',
|
|
};
|
|
|
|
if (this.localProxy?.enabled) {
|
|
config = {
|
|
...config,
|
|
httpsAgent: makeProxyAgent({
|
|
host: this.localProxy.host,
|
|
port: this.localProxy.port,
|
|
protocol: this.localProxy.protocol,
|
|
username: this.localProxy.username,
|
|
password: this.localProxy.password,
|
|
}),
|
|
};
|
|
}
|
|
|
|
const response = await axios.get(url, config);
|
|
imageBuffer = Buffer.from(response.data, 'binary');
|
|
}
|
|
|
|
const isAnimated = this.isAnimated(image, imageBuffer);
|
|
|
|
if (isAnimated) {
|
|
return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer();
|
|
} else {
|
|
return await sharp(imageBuffer).webp().toBuffer();
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao converter a imagem para WebP:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private isAnimatedWebp(buffer: Buffer): boolean {
|
|
if (buffer.length < 12) return false;
|
|
|
|
return buffer.indexOf(Buffer.from('ANIM')) !== -1;
|
|
}
|
|
|
|
private isAnimated(image: string, buffer: Buffer): boolean {
|
|
const lowerCaseImage = image.toLowerCase();
|
|
|
|
if (lowerCaseImage.includes('.gif')) return true;
|
|
|
|
if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer);
|
|
|
|
return false;
|
|
}
|
|
|
|
public async mediaSticker(data: SendStickerDto, file?: any) {
|
|
const mediaData: SendStickerDto = { ...data };
|
|
|
|
if (file) mediaData.sticker = file.buffer.toString('base64');
|
|
|
|
const convert = data?.notConvertSticker
|
|
? Buffer.from(data.sticker, 'base64')
|
|
: await this.convertToWebP(data.sticker);
|
|
const gifPlayback = data.sticker.includes('.gif');
|
|
const result = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
sticker: convert,
|
|
gifPlayback,
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
public async mediaMessage(data: SendMediaDto, file?: any, isIntegration = false) {
|
|
const mediaData: SendMediaDto = { ...data };
|
|
|
|
if (file) mediaData.media = file.buffer.toString('base64');
|
|
|
|
const generate = await this.prepareMediaMessage(mediaData);
|
|
|
|
const mediaSent = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{ ...generate.message },
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
isIntegration,
|
|
);
|
|
|
|
return mediaSent;
|
|
}
|
|
|
|
public async ptvMessage(data: SendPtvDto, file?: any, isIntegration = false) {
|
|
const mediaData: SendMediaDto = {
|
|
number: data.number,
|
|
media: data.video,
|
|
mediatype: 'ptv',
|
|
delay: data?.delay,
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
};
|
|
|
|
if (file) mediaData.media = file.buffer.toString('base64');
|
|
|
|
const generate = await this.prepareMediaMessage(mediaData);
|
|
|
|
const mediaSent = await this.sendMessageWithTyping(
|
|
data.number,
|
|
{ ...generate.message },
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
isIntegration,
|
|
);
|
|
|
|
return mediaSent;
|
|
}
|
|
|
|
public async processAudioMp4(audio: string) {
|
|
let inputStream: PassThrough;
|
|
|
|
if (isURL(audio)) {
|
|
const response = await axios.get(audio, { responseType: 'stream' });
|
|
inputStream = response.data;
|
|
} else {
|
|
const audioBuffer = Buffer.from(audio, 'base64');
|
|
inputStream = new PassThrough();
|
|
inputStream.end(audioBuffer);
|
|
}
|
|
|
|
return new Promise<Buffer>((resolve, reject) => {
|
|
const ffmpegProcess = spawn(ffmpegPath.path, [
|
|
'-i',
|
|
'pipe:0',
|
|
'-vn',
|
|
'-ab',
|
|
'128k',
|
|
'-ar',
|
|
'44100',
|
|
'-f',
|
|
'mp4',
|
|
'-movflags',
|
|
'frag_keyframe+empty_moov',
|
|
'pipe:1',
|
|
]);
|
|
|
|
const outputChunks: Buffer[] = [];
|
|
let stderrData = '';
|
|
|
|
ffmpegProcess.stdout.on('data', (chunk) => {
|
|
outputChunks.push(chunk);
|
|
});
|
|
|
|
ffmpegProcess.stderr.on('data', (data) => {
|
|
stderrData += data.toString();
|
|
this.logger.verbose(`ffmpeg stderr: ${data}`);
|
|
});
|
|
|
|
ffmpegProcess.on('error', (error) => {
|
|
console.error('Error in ffmpeg process', error);
|
|
reject(error);
|
|
});
|
|
|
|
ffmpegProcess.on('close', (code) => {
|
|
if (code === 0) {
|
|
this.logger.verbose('Audio converted to mp4');
|
|
const outputBuffer = Buffer.concat(outputChunks);
|
|
resolve(outputBuffer);
|
|
} else {
|
|
this.logger.error(`ffmpeg exited with code ${code}`);
|
|
this.logger.error(`ffmpeg stderr: ${stderrData}`);
|
|
reject(new Error(`ffmpeg exited with code ${code}: ${stderrData}`));
|
|
}
|
|
});
|
|
|
|
inputStream.pipe(ffmpegProcess.stdin);
|
|
|
|
inputStream.on('error', (err) => {
|
|
console.error('Error in inputStream', err);
|
|
ffmpegProcess.stdin.end();
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
public async processAudio(audio: string): Promise<Buffer> {
|
|
if (process.env.API_AUDIO_CONVERTER) {
|
|
this.logger.verbose('Using audio converter API');
|
|
const formData = new FormData();
|
|
|
|
if (isURL(audio)) {
|
|
formData.append('url', audio);
|
|
} else {
|
|
formData.append('base64', audio);
|
|
}
|
|
|
|
const { data } = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
|
|
headers: {
|
|
...formData.getHeaders(),
|
|
apikey: process.env.API_AUDIO_CONVERTER_KEY,
|
|
},
|
|
});
|
|
|
|
if (!data.audio) {
|
|
throw new InternalServerErrorException('Failed to convert audio');
|
|
}
|
|
|
|
this.logger.verbose('Audio converted');
|
|
return Buffer.from(data.audio, 'base64');
|
|
} else {
|
|
let inputAudioStream: PassThrough;
|
|
|
|
if (isURL(audio)) {
|
|
const timestamp = new Date().getTime();
|
|
const url = `${audio}?timestamp=${timestamp}`;
|
|
|
|
const config: any = {
|
|
responseType: 'stream',
|
|
};
|
|
|
|
const response = await axios.get(url, config);
|
|
inputAudioStream = response.data.pipe(new PassThrough());
|
|
} else {
|
|
const audioBuffer = Buffer.from(audio, 'base64');
|
|
inputAudioStream = new PassThrough();
|
|
inputAudioStream.end(audioBuffer);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const outputAudioStream = new PassThrough();
|
|
const chunks: Buffer[] = [];
|
|
|
|
outputAudioStream.on('data', (chunk) => chunks.push(chunk));
|
|
outputAudioStream.on('end', () => {
|
|
const outputBuffer = Buffer.concat(chunks);
|
|
resolve(outputBuffer);
|
|
});
|
|
|
|
outputAudioStream.on('error', (error) => {
|
|
console.log('error', error);
|
|
reject(error);
|
|
});
|
|
|
|
ffmpeg.setFfmpegPath(ffmpegPath.path);
|
|
|
|
ffmpeg(inputAudioStream)
|
|
.outputFormat('ogg')
|
|
.noVideo()
|
|
.audioCodec('libopus')
|
|
.addOutputOptions('-avoid_negative_ts make_zero')
|
|
.audioBitrate('128k')
|
|
.audioFrequency(48000)
|
|
.audioChannels(1)
|
|
.outputOptions([
|
|
'-write_xing',
|
|
'0',
|
|
'-compression_level',
|
|
'10',
|
|
'-application',
|
|
'voip',
|
|
'-fflags',
|
|
'+bitexact',
|
|
'-flags',
|
|
'+bitexact',
|
|
'-id3v2_version',
|
|
'0',
|
|
'-map_metadata',
|
|
'-1',
|
|
'-map_chapters',
|
|
'-1',
|
|
'-write_bext',
|
|
'0',
|
|
])
|
|
.pipe(outputAudioStream, { end: true })
|
|
.on('error', function (error) {
|
|
console.log('error', error);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
|
const mediaData: SendAudioDto = { ...data };
|
|
|
|
if (file?.buffer) {
|
|
mediaData.audio = file.buffer.toString('base64');
|
|
} else if (!isURL(data.audio) && !isBase64(data.audio)) {
|
|
console.error('Invalid file or audio source');
|
|
throw new BadRequestException('File buffer, URL, or base64 audio is required');
|
|
}
|
|
|
|
if (!data?.encoding && data?.encoding !== false) {
|
|
data.encoding = true;
|
|
}
|
|
|
|
if (data?.encoding) {
|
|
const convert = await this.processAudio(mediaData.audio);
|
|
|
|
if (Buffer.isBuffer(convert)) {
|
|
const result = this.sendMessageWithTyping<AnyMessageContent>(
|
|
data.number,
|
|
{
|
|
audio: convert,
|
|
ptt: true,
|
|
mimetype: 'audio/ogg; codecs=opus',
|
|
},
|
|
{ presence: 'recording', delay: data?.delay },
|
|
isIntegration,
|
|
);
|
|
|
|
return result;
|
|
} else {
|
|
throw new InternalServerErrorException('Failed to convert audio');
|
|
}
|
|
}
|
|
|
|
return await this.sendMessageWithTyping<AnyMessageContent>(
|
|
data.number,
|
|
{
|
|
audio: isURL(data.audio) ? { url: data.audio } : Buffer.from(data.audio, 'base64'),
|
|
ptt: true,
|
|
mimetype: 'audio/ogg; codecs=opus',
|
|
},
|
|
{ presence: 'recording', delay: data?.delay },
|
|
isIntegration,
|
|
);
|
|
}
|
|
|
|
private generateRandomId(length = 11) {
|
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
let result = '';
|
|
for (let i = 0; i < length; i++) {
|
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private toJSONString(button: Button): string {
|
|
const toString = (obj: any) => JSON.stringify(obj);
|
|
|
|
const json = {
|
|
call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
|
|
reply: () => toString({ display_text: button.displayText, id: button.id }),
|
|
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
|
|
url: () =>
|
|
toString({
|
|
display_text: button.displayText,
|
|
url: button.url,
|
|
merchant_url: button.url,
|
|
}),
|
|
pix: () =>
|
|
toString({
|
|
currency: button.currency,
|
|
total_amount: {
|
|
value: 0,
|
|
offset: 100,
|
|
},
|
|
reference_id: this.generateRandomId(),
|
|
type: 'physical-goods',
|
|
order: {
|
|
status: 'pending',
|
|
subtotal: {
|
|
value: 0,
|
|
offset: 100,
|
|
},
|
|
order_type: 'ORDER',
|
|
items: [
|
|
{
|
|
name: '',
|
|
amount: {
|
|
value: 0,
|
|
offset: 100,
|
|
},
|
|
quantity: 0,
|
|
sale_amount: {
|
|
value: 0,
|
|
offset: 100,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
payment_settings: [
|
|
{
|
|
type: 'pix_static_code',
|
|
pix_static_code: {
|
|
merchant_name: button.name,
|
|
key: button.key,
|
|
key_type: this.mapKeyType.get(button.keyType),
|
|
},
|
|
},
|
|
],
|
|
share_payment_status: false,
|
|
}),
|
|
};
|
|
|
|
return json[button.type]?.() || '';
|
|
}
|
|
|
|
private readonly mapType = new Map<TypeButton, string>([
|
|
['reply', 'quick_reply'],
|
|
['copy', 'cta_copy'],
|
|
['url', 'cta_url'],
|
|
['call', 'cta_call'],
|
|
['pix', 'payment_info'],
|
|
]);
|
|
|
|
private readonly mapKeyType = new Map<KeyType, string>([
|
|
['phone', 'PHONE'],
|
|
['email', 'EMAIL'],
|
|
['cpf', 'CPF'],
|
|
['cnpj', 'CNPJ'],
|
|
['random', 'EVP'],
|
|
]);
|
|
|
|
public async buttonMessage(data: SendButtonsDto) {
|
|
if (data.buttons.length === 0) {
|
|
throw new BadRequestException('At least one button is required');
|
|
}
|
|
|
|
const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply');
|
|
|
|
const hasPixButton = data.buttons.some((btn) => btn.type === 'pix');
|
|
|
|
const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix');
|
|
|
|
if (hasReplyButtons) {
|
|
if (data.buttons.length > 3) {
|
|
throw new BadRequestException('Maximum of 3 reply buttons allowed');
|
|
}
|
|
if (hasOtherButtons) {
|
|
throw new BadRequestException('Reply buttons cannot be mixed with other button types');
|
|
}
|
|
}
|
|
|
|
if (hasPixButton) {
|
|
if (data.buttons.length > 1) {
|
|
throw new BadRequestException('Only one PIX button is allowed');
|
|
}
|
|
if (hasOtherButtons) {
|
|
throw new BadRequestException('PIX button cannot be mixed with other button types');
|
|
}
|
|
|
|
const message: proto.IMessage = {
|
|
viewOnceMessage: {
|
|
message: {
|
|
interactiveMessage: {
|
|
nativeFlowMessage: {
|
|
buttons: [
|
|
{
|
|
name: this.mapType.get('pix'),
|
|
buttonParamsJson: this.toJSONString(data.buttons[0]),
|
|
},
|
|
],
|
|
messageParamsJson: JSON.stringify({
|
|
from: 'api',
|
|
templateId: v4(),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return await this.sendMessageWithTyping(data.number, message, {
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
});
|
|
}
|
|
|
|
const generate = await (async () => {
|
|
if (data?.thumbnailUrl) {
|
|
return await this.prepareMediaMessage({
|
|
mediatype: 'image',
|
|
media: data.thumbnailUrl,
|
|
});
|
|
}
|
|
})();
|
|
|
|
const buttons = data.buttons.map((value) => {
|
|
return {
|
|
name: this.mapType.get(value.type),
|
|
buttonParamsJson: this.toJSONString(value),
|
|
};
|
|
});
|
|
|
|
const message: proto.IMessage = {
|
|
viewOnceMessage: {
|
|
message: {
|
|
interactiveMessage: {
|
|
body: {
|
|
text: (() => {
|
|
let t = '*' + data.title + '*';
|
|
if (data?.description) {
|
|
t += '\n\n';
|
|
t += data.description;
|
|
t += '\n';
|
|
}
|
|
return t;
|
|
})(),
|
|
},
|
|
footer: {
|
|
text: data?.footer,
|
|
},
|
|
header: (() => {
|
|
if (generate?.message?.imageMessage) {
|
|
return {
|
|
hasMediaAttachment: !!generate.message.imageMessage,
|
|
imageMessage: generate.message.imageMessage,
|
|
};
|
|
}
|
|
})(),
|
|
nativeFlowMessage: {
|
|
buttons: buttons,
|
|
messageParamsJson: JSON.stringify({
|
|
from: 'api',
|
|
templateId: v4(),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return await this.sendMessageWithTyping(data.number, message, {
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
});
|
|
}
|
|
|
|
public async locationMessage(data: SendLocationDto) {
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
locationMessage: {
|
|
degreesLatitude: data.latitude,
|
|
degreesLongitude: data.longitude,
|
|
name: data?.name,
|
|
address: data?.address,
|
|
},
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async listMessage(data: SendListDto) {
|
|
return await this.sendMessageWithTyping(
|
|
data.number,
|
|
{
|
|
listMessage: {
|
|
title: data.title,
|
|
description: data.description,
|
|
buttonText: data?.buttonText,
|
|
footerText: data?.footerText,
|
|
sections: data.sections,
|
|
listType: 2,
|
|
},
|
|
},
|
|
{
|
|
delay: data?.delay,
|
|
presence: 'composing',
|
|
quoted: data?.quoted,
|
|
mentionsEveryOne: data?.mentionsEveryOne,
|
|
mentioned: data?.mentioned,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async contactMessage(data: SendContactDto) {
|
|
const message: proto.IMessage = {};
|
|
|
|
const vcard = (contact: ContactMessage) => {
|
|
let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`;
|
|
|
|
if (contact.organization) {
|
|
result += `ORG:${contact.organization};\n`;
|
|
}
|
|
|
|
if (contact.email) {
|
|
result += `EMAIL:${contact.email}\n`;
|
|
}
|
|
|
|
if (contact.url) {
|
|
result += `URL:${contact.url}\n`;
|
|
}
|
|
|
|
if (!contact.wuid) {
|
|
contact.wuid = createJid(contact.phoneNumber);
|
|
}
|
|
|
|
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';
|
|
|
|
return result;
|
|
};
|
|
|
|
if (data.contact.length === 1) {
|
|
message.contactMessage = {
|
|
displayName: data.contact[0].fullName,
|
|
vcard: vcard(data.contact[0]),
|
|
};
|
|
} else {
|
|
message.contactsArrayMessage = {
|
|
displayName: `${data.contact.length} contacts`,
|
|
contacts: data.contact.map((contact) => {
|
|
return {
|
|
displayName: contact.fullName,
|
|
vcard: vcard(contact),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
return await this.sendMessageWithTyping(data.number, { ...message }, {});
|
|
}
|
|
|
|
public async reactionMessage(data: SendReactionDto) {
|
|
return await this.sendMessageWithTyping(data.key.remoteJid, {
|
|
reactionMessage: {
|
|
key: data.key,
|
|
text: data.reaction,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Chat Controller
|
|
public async whatsappNumber(data: WhatsAppNumberDto) {
|
|
const jids: {
|
|
groups: { number: string; jid: string }[];
|
|
broadcast: { number: string; jid: string }[];
|
|
users: { number: string; jid: string; name?: string }[];
|
|
} = {
|
|
groups: [],
|
|
broadcast: [],
|
|
users: [],
|
|
};
|
|
|
|
data.numbers.forEach((number) => {
|
|
const jid = createJid(number);
|
|
|
|
if (isJidGroup(jid)) {
|
|
jids.groups.push({ number, jid });
|
|
} else if (jid === 'status@broadcast') {
|
|
jids.broadcast.push({ number, jid });
|
|
} else {
|
|
jids.users.push({ number, jid });
|
|
}
|
|
});
|
|
|
|
const onWhatsapp: OnWhatsAppDto[] = [];
|
|
|
|
// BROADCAST
|
|
onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number)));
|
|
|
|
// GROUPS
|
|
const groups = await Promise.all(
|
|
jids.groups.map(async ({ jid, number }) => {
|
|
const group = await this.findGroup({ groupJid: jid }, 'inner');
|
|
|
|
if (!group) {
|
|
new OnWhatsAppDto(jid, false, number);
|
|
}
|
|
|
|
return new OnWhatsAppDto(group.id, !!group?.id, number, group?.subject);
|
|
}),
|
|
);
|
|
onWhatsapp.push(...groups);
|
|
|
|
// USERS
|
|
const contacts: any[] = await this.prismaRepository.contact.findMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
remoteJid: {
|
|
in: jids.users.map(({ jid }) => jid),
|
|
},
|
|
},
|
|
});
|
|
|
|
const numbersToVerify = jids.users.map(({ jid }) => jid.replace('+', ''));
|
|
|
|
const cachedNumbers = await getOnWhatsappCache(numbersToVerify);
|
|
const filteredNumbers = numbersToVerify.filter(
|
|
(jid) => !cachedNumbers.some((cached) => cached.jidOptions.includes(jid)),
|
|
);
|
|
|
|
const verify = await this.client.onWhatsApp(...filteredNumbers);
|
|
const users: OnWhatsAppDto[] = await Promise.all(
|
|
jids.users.map(async (user) => {
|
|
let numberVerified: (typeof verify)[0] | null = null;
|
|
|
|
const cached = cachedNumbers.find((cached) => cached.jidOptions.includes(user.jid.replace('+', '')));
|
|
if (cached) {
|
|
return {
|
|
exists: true,
|
|
jid: cached.remoteJid,
|
|
name: contacts.find((c) => c.remoteJid === cached.remoteJid)?.pushName,
|
|
number: user.number,
|
|
};
|
|
}
|
|
|
|
// Brazilian numbers
|
|
if (user.number.startsWith('55')) {
|
|
const numberWithDigit =
|
|
user.number.slice(4, 5) === '9' && user.number.length === 13
|
|
? user.number
|
|
: `${user.number.slice(0, 4)}9${user.number.slice(4)}`;
|
|
const numberWithoutDigit =
|
|
user.number.length === 12 ? user.number : user.number.slice(0, 4) + user.number.slice(5);
|
|
|
|
numberVerified = verify.find(
|
|
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
|
|
);
|
|
}
|
|
|
|
// Mexican/Argentina numbers
|
|
// Ref: https://faq.whatsapp.com/1294841057948784
|
|
if (!numberVerified && (user.number.startsWith('52') || user.number.startsWith('54'))) {
|
|
let prefix = '';
|
|
if (user.number.startsWith('52')) {
|
|
prefix = '';
|
|
}
|
|
if (user.number.startsWith('54')) {
|
|
prefix = '9';
|
|
}
|
|
|
|
const numberWithDigit =
|
|
user.number.slice(2, 3) === prefix && user.number.length === 13
|
|
? user.number
|
|
: `${user.number.slice(0, 2)}${prefix}${user.number.slice(2)}`;
|
|
const numberWithoutDigit =
|
|
user.number.length === 12 ? user.number : user.number.slice(0, 2) + user.number.slice(3);
|
|
|
|
numberVerified = verify.find(
|
|
(v) => v.jid === `${numberWithDigit}@s.whatsapp.net` || v.jid === `${numberWithoutDigit}@s.whatsapp.net`,
|
|
);
|
|
}
|
|
|
|
if (!numberVerified) {
|
|
numberVerified = verify.find((v) => v.jid === user.jid);
|
|
}
|
|
|
|
const numberJid = numberVerified?.jid || user.jid;
|
|
|
|
return {
|
|
exists: !!numberVerified?.exists,
|
|
jid: numberJid,
|
|
name: contacts.find((c) => c.remoteJid === numberJid)?.pushName,
|
|
number: user.number,
|
|
};
|
|
}),
|
|
);
|
|
|
|
await saveOnWhatsappCache(users.filter((user) => user.exists).map((user) => ({ remoteJid: user.jid })));
|
|
|
|
onWhatsapp.push(...users);
|
|
|
|
return onWhatsapp;
|
|
}
|
|
|
|
public async markMessageAsRead(data: ReadMessageDto) {
|
|
try {
|
|
const keys: proto.IMessageKey[] = [];
|
|
data.readMessages.forEach((read) => {
|
|
if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) {
|
|
keys.push({
|
|
remoteJid: read.remoteJid,
|
|
fromMe: read.fromMe,
|
|
id: read.id,
|
|
});
|
|
}
|
|
});
|
|
await this.client.readMessages(keys);
|
|
return { message: 'Read messages', read: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Read messages fail', error.toString());
|
|
}
|
|
}
|
|
|
|
public async getLastMessage(number: string) {
|
|
const where: any = {
|
|
key: {
|
|
remoteJid: number,
|
|
},
|
|
instanceId: this.instance.id,
|
|
};
|
|
|
|
const messages = await this.prismaRepository.message.findMany({
|
|
where,
|
|
orderBy: {
|
|
messageTimestamp: 'desc',
|
|
},
|
|
take: 1,
|
|
});
|
|
|
|
if (messages.length === 0) {
|
|
throw new NotFoundException('Messages not found');
|
|
}
|
|
|
|
let lastMessage = messages.pop();
|
|
|
|
for (const message of messages) {
|
|
if (message.messageTimestamp >= lastMessage.messageTimestamp) {
|
|
lastMessage = message;
|
|
}
|
|
}
|
|
|
|
return lastMessage as unknown as LastMessage;
|
|
}
|
|
|
|
public async archiveChat(data: ArchiveChatDto) {
|
|
try {
|
|
let last_message = data.lastMessage;
|
|
let number = data.chat;
|
|
|
|
if (!last_message && number) {
|
|
last_message = await this.getLastMessage(number);
|
|
} else {
|
|
last_message = data.lastMessage;
|
|
last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now();
|
|
number = last_message?.key?.remoteJid;
|
|
}
|
|
|
|
if (!last_message || Object.keys(last_message).length === 0) {
|
|
throw new NotFoundException('Last message not found');
|
|
}
|
|
|
|
await this.client.chatModify(
|
|
{
|
|
archive: data.archive,
|
|
lastMessages: [last_message],
|
|
},
|
|
createJid(number),
|
|
);
|
|
|
|
return {
|
|
chatId: number,
|
|
archived: true,
|
|
};
|
|
} catch (error) {
|
|
throw new InternalServerErrorException({
|
|
archived: false,
|
|
message: ['An error occurred while archiving the chat. Open a calling.', error.toString()],
|
|
});
|
|
}
|
|
}
|
|
|
|
public async markChatUnread(data: MarkChatUnreadDto) {
|
|
try {
|
|
let last_message = data.lastMessage;
|
|
let number = data.chat;
|
|
|
|
if (!last_message && number) {
|
|
last_message = await this.getLastMessage(number);
|
|
} else {
|
|
last_message = data.lastMessage;
|
|
last_message.messageTimestamp = last_message?.messageTimestamp ?? Date.now();
|
|
number = last_message?.key?.remoteJid;
|
|
}
|
|
|
|
if (!last_message || Object.keys(last_message).length === 0) {
|
|
throw new NotFoundException('Last message not found');
|
|
}
|
|
|
|
await this.client.chatModify(
|
|
{
|
|
markRead: false,
|
|
lastMessages: [last_message],
|
|
},
|
|
createJid(number),
|
|
);
|
|
|
|
return {
|
|
chatId: number,
|
|
markedChatUnread: true,
|
|
};
|
|
} catch (error) {
|
|
throw new InternalServerErrorException({
|
|
markedChatUnread: false,
|
|
message: ['An error occurred while marked unread the chat. Open a calling.', error.toString()],
|
|
});
|
|
}
|
|
}
|
|
|
|
public async deleteMessage(del: DeleteMessage) {
|
|
try {
|
|
const response = await this.client.sendMessage(del.remoteJid, { delete: del });
|
|
if (response) {
|
|
const messageId = response.message?.protocolMessage?.key?.id;
|
|
if (messageId) {
|
|
const isLogicalDeleted = configService.get<Database>('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE;
|
|
let message = await this.prismaRepository.message.findFirst({
|
|
where: {
|
|
key: {
|
|
path: ['id'],
|
|
equals: messageId,
|
|
},
|
|
},
|
|
});
|
|
if (isLogicalDeleted) {
|
|
if (!message) return response;
|
|
const existingKey = typeof message?.key === 'object' && message.key !== null ? message.key : {};
|
|
message = await this.prismaRepository.message.update({
|
|
where: { id: message.id },
|
|
data: {
|
|
key: {
|
|
...existingKey,
|
|
deleted: true,
|
|
},
|
|
status: 'DELETED',
|
|
},
|
|
});
|
|
const messageUpdate: any = {
|
|
messageId: message.id,
|
|
keyId: messageId,
|
|
remoteJid: response.key.remoteJid,
|
|
fromMe: response.key.fromMe,
|
|
participant: response.key?.remoteJid,
|
|
status: 'DELETED',
|
|
instanceId: this.instanceId,
|
|
};
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: messageUpdate,
|
|
});
|
|
} else {
|
|
await this.prismaRepository.message.deleteMany({
|
|
where: {
|
|
id: message.id,
|
|
},
|
|
});
|
|
}
|
|
this.sendDataWebhook(Events.MESSAGES_DELETE, {
|
|
id: message.id,
|
|
instanceId: message.instanceId,
|
|
key: message.key,
|
|
messageType: message.messageType,
|
|
status: 'DELETED',
|
|
source: message.source,
|
|
messageTimestamp: message.messageTimestamp,
|
|
pushName: message.pushName,
|
|
participant: message.participant,
|
|
message: message.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString());
|
|
}
|
|
}
|
|
|
|
public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
|
|
try {
|
|
const m = data?.message;
|
|
const convertToMp4 = data?.convertToMp4 ?? false;
|
|
|
|
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo);
|
|
|
|
if (!msg) {
|
|
throw 'Message not found';
|
|
}
|
|
|
|
for (const subtype of MessageSubtype) {
|
|
if (msg.message[subtype]) {
|
|
msg.message = msg.message[subtype].message;
|
|
}
|
|
}
|
|
|
|
let mediaMessage: any;
|
|
let mediaType: string;
|
|
|
|
for (const type of TypeMediaMessage) {
|
|
mediaMessage = msg.message[type];
|
|
if (mediaMessage) {
|
|
mediaType = type;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!mediaMessage) {
|
|
throw 'The message is not of the media type';
|
|
}
|
|
|
|
if (typeof mediaMessage['mediaKey'] === 'object') {
|
|
msg.message = JSON.parse(JSON.stringify(msg.message));
|
|
}
|
|
|
|
const buffer = await downloadMediaMessage(
|
|
{ key: msg?.key, message: msg?.message },
|
|
'buffer',
|
|
{},
|
|
{
|
|
logger: P({ level: 'error' }) as any,
|
|
reuploadRequest: this.client.updateMediaMessage,
|
|
},
|
|
);
|
|
const typeMessage = getContentType(msg.message);
|
|
|
|
const ext = mimeTypes.extension(mediaMessage?.['mimetype']);
|
|
const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`;
|
|
|
|
if (convertToMp4 && typeMessage === 'audioMessage') {
|
|
try {
|
|
const convert = await this.processAudioMp4(buffer.toString('base64'));
|
|
|
|
if (Buffer.isBuffer(convert)) {
|
|
const result = {
|
|
mediaType,
|
|
fileName,
|
|
caption: mediaMessage['caption'],
|
|
size: {
|
|
fileLength: mediaMessage['fileLength'],
|
|
height: mediaMessage['height'],
|
|
width: mediaMessage['width'],
|
|
},
|
|
mimetype: 'audio/mp4',
|
|
base64: convert.toString('base64'),
|
|
buffer: getBuffer ? convert : null,
|
|
};
|
|
|
|
return result;
|
|
}
|
|
} catch (error) {
|
|
this.logger.error('Error converting audio to mp4:');
|
|
this.logger.error(error);
|
|
throw new BadRequestException('Failed to convert audio to MP4');
|
|
}
|
|
}
|
|
|
|
return {
|
|
mediaType,
|
|
fileName,
|
|
caption: mediaMessage['caption'],
|
|
size: {
|
|
fileLength: mediaMessage['fileLength'],
|
|
height: mediaMessage['height'],
|
|
width: mediaMessage['width'],
|
|
},
|
|
mimetype: mediaMessage['mimetype'],
|
|
base64: buffer.toString('base64'),
|
|
buffer: getBuffer ? buffer : null,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error('Error processing media message:');
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
public async fetchPrivacySettings() {
|
|
const privacy = await this.client.fetchPrivacySettings();
|
|
|
|
return {
|
|
readreceipts: privacy.readreceipts,
|
|
profile: privacy.profile,
|
|
status: privacy.status,
|
|
online: privacy.online,
|
|
last: privacy.last,
|
|
groupadd: privacy.groupadd,
|
|
};
|
|
}
|
|
|
|
public async updatePrivacySettings(settings: PrivacySettingDto) {
|
|
try {
|
|
await this.client.updateReadReceiptsPrivacy(settings.readreceipts);
|
|
await this.client.updateProfilePicturePrivacy(settings.profile);
|
|
await this.client.updateStatusPrivacy(settings.status);
|
|
await this.client.updateOnlinePrivacy(settings.online);
|
|
await this.client.updateLastSeenPrivacy(settings.last);
|
|
await this.client.updateGroupsAddPrivacy(settings.groupadd);
|
|
|
|
this.reloadConnection();
|
|
|
|
return {
|
|
update: 'success',
|
|
data: {
|
|
readreceipts: settings.readreceipts,
|
|
profile: settings.profile,
|
|
status: settings.status,
|
|
online: settings.online,
|
|
last: settings.last,
|
|
groupadd: settings.groupadd,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating privacy settings', error.toString());
|
|
}
|
|
}
|
|
|
|
public async fetchBusinessProfile(number: string): Promise<NumberBusiness> {
|
|
try {
|
|
const jid = number ? createJid(number) : this.instance.wuid;
|
|
|
|
const profile = await this.client.getBusinessProfile(jid);
|
|
|
|
if (!profile) {
|
|
const info = await this.whatsappNumber({ numbers: [jid] });
|
|
|
|
return {
|
|
isBusiness: false,
|
|
message: 'Not is business profile',
|
|
...info?.shift(),
|
|
};
|
|
}
|
|
|
|
return {
|
|
isBusiness: true,
|
|
...profile,
|
|
};
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating profile name', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateProfileName(name: string) {
|
|
try {
|
|
await this.client.updateProfileName(name);
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating profile name', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateProfileStatus(status: string) {
|
|
try {
|
|
await this.client.updateProfileStatus(status);
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating profile status', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateProfilePicture(picture: string) {
|
|
try {
|
|
let pic: WAMediaUpload;
|
|
if (isURL(picture)) {
|
|
const timestamp = new Date().getTime();
|
|
const url = `${picture}?timestamp=${timestamp}`;
|
|
|
|
let config: any = {
|
|
responseType: 'arraybuffer',
|
|
};
|
|
|
|
if (this.localProxy?.enabled) {
|
|
config = {
|
|
...config,
|
|
httpsAgent: makeProxyAgent({
|
|
host: this.localProxy.host,
|
|
port: this.localProxy.port,
|
|
protocol: this.localProxy.protocol,
|
|
username: this.localProxy.username,
|
|
password: this.localProxy.password,
|
|
}),
|
|
};
|
|
}
|
|
|
|
pic = (await axios.get(url, config)).data;
|
|
} else if (isBase64(picture)) {
|
|
pic = Buffer.from(picture, 'base64');
|
|
} else {
|
|
throw new BadRequestException('"profilePicture" must be a url or a base64');
|
|
}
|
|
|
|
await this.client.updateProfilePicture(this.instance.wuid, pic);
|
|
|
|
this.reloadConnection();
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating profile picture', error.toString());
|
|
}
|
|
}
|
|
|
|
public async removeProfilePicture() {
|
|
try {
|
|
await this.client.removeProfilePicture(this.instance.wuid);
|
|
|
|
this.reloadConnection();
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error removing profile picture', error.toString());
|
|
}
|
|
}
|
|
|
|
public async blockUser(data: BlockUserDto) {
|
|
try {
|
|
const { number } = data;
|
|
|
|
const isWA = (await this.whatsappNumber({ numbers: [number] }))?.shift();
|
|
|
|
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
|
|
throw new BadRequestException(isWA);
|
|
}
|
|
|
|
const sender = isWA.jid;
|
|
|
|
await this.client.updateBlockStatus(sender, data.status);
|
|
|
|
return { block: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error blocking user', error.toString());
|
|
}
|
|
}
|
|
|
|
private async formatUpdateMessage(data: UpdateMessageDto) {
|
|
try {
|
|
const msg: any = await this.getMessage(data.key, true);
|
|
|
|
if (msg?.messageType === 'conversation' || msg?.messageType === 'extendedTextMessage') {
|
|
return {
|
|
text: data.text,
|
|
};
|
|
}
|
|
|
|
if (msg?.messageType === 'imageMessage') {
|
|
return {
|
|
image: msg?.message?.imageMessage,
|
|
caption: data.text,
|
|
};
|
|
}
|
|
|
|
if (msg?.messageType === 'videoMessage') {
|
|
return {
|
|
video: msg?.message?.videoMessage,
|
|
caption: data.text,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new BadRequestException(error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateMessage(data: UpdateMessageDto) {
|
|
const jid = createJid(data.number);
|
|
|
|
const options = await this.formatUpdateMessage(data);
|
|
|
|
if (!options) {
|
|
this.logger.error('Message not compatible');
|
|
throw new BadRequestException('Message not compatible');
|
|
}
|
|
|
|
try {
|
|
const oldMessage: any = await this.getMessage(data.key, true);
|
|
if (!oldMessage) throw new NotFoundException('Message not found');
|
|
if (oldMessage?.key?.remoteJid !== jid) {
|
|
throw new BadRequestException('RemoteJid does not match');
|
|
}
|
|
if (oldMessage?.messageTimestamp > Date.now() + 900000) {
|
|
// 15 minutes in milliseconds
|
|
throw new BadRequestException('Message is older than 15 minutes');
|
|
}
|
|
|
|
const messageSent = await this.client.sendMessage(jid, {
|
|
...(options as any),
|
|
edit: data.key,
|
|
});
|
|
if (messageSent) {
|
|
const editedMessage =
|
|
messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage;
|
|
|
|
if (editedMessage) {
|
|
this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage);
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
|
|
this.chatwootService.eventWhatsapp(
|
|
'send.message.update',
|
|
{ instanceName: this.instance.name, instanceId: this.instance.id },
|
|
editedMessage,
|
|
);
|
|
|
|
const messageId = messageSent.message?.protocolMessage?.key?.id;
|
|
if (messageId) {
|
|
let message = await this.prismaRepository.message.findFirst({
|
|
where: {
|
|
key: {
|
|
path: ['id'],
|
|
equals: messageId,
|
|
},
|
|
},
|
|
});
|
|
if (!message) throw new NotFoundException('Message not found');
|
|
|
|
if (!(message.key.valueOf() as any).fromMe) {
|
|
new BadRequestException('You cannot edit others messages');
|
|
}
|
|
if ((message.key.valueOf() as any)?.deleted) {
|
|
new BadRequestException('You cannot edit deleted messages');
|
|
}
|
|
if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') {
|
|
oldMessage.message.conversation = data.text;
|
|
} else {
|
|
oldMessage.message[oldMessage.messageType].caption = data.text;
|
|
}
|
|
message = await this.prismaRepository.message.update({
|
|
where: { id: message.id },
|
|
data: {
|
|
message: oldMessage.message,
|
|
status: 'EDITED',
|
|
messageTimestamp: Math.floor(Date.now() / 1000), // Convert to int32 by dividing by 1000 to get seconds
|
|
},
|
|
});
|
|
const messageUpdate: any = {
|
|
messageId: message.id,
|
|
keyId: messageId,
|
|
remoteJid: messageSent.key.remoteJid,
|
|
fromMe: messageSent.key.fromMe,
|
|
participant: messageSent.key?.remoteJid,
|
|
status: 'EDITED',
|
|
instanceId: this.instanceId,
|
|
};
|
|
await this.prismaRepository.messageUpdate.create({
|
|
data: messageUpdate,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return messageSent;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async fetchLabels(): Promise<LabelDto[]> {
|
|
const labels = await this.prismaRepository.label.findMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
},
|
|
});
|
|
|
|
return labels.map((label) => ({
|
|
color: label.color,
|
|
name: label.name,
|
|
id: label.labelId,
|
|
predefinedId: label.predefinedId,
|
|
}));
|
|
}
|
|
|
|
public async handleLabel(data: HandleLabelDto) {
|
|
const whatsappContact = await this.whatsappNumber({ numbers: [data.number] });
|
|
if (whatsappContact.length === 0) {
|
|
throw new NotFoundException('Number not found');
|
|
}
|
|
const contact = whatsappContact[0];
|
|
if (!contact.exists) {
|
|
throw new NotFoundException('Number is not on WhatsApp');
|
|
}
|
|
|
|
try {
|
|
if (data.action === 'add') {
|
|
await this.client.addChatLabel(contact.jid, data.labelId);
|
|
await this.addLabel(data.labelId, this.instanceId, contact.jid);
|
|
|
|
return { numberJid: contact.jid, labelId: data.labelId, add: true };
|
|
}
|
|
if (data.action === 'remove') {
|
|
await this.client.removeChatLabel(contact.jid, data.labelId);
|
|
await this.removeLabel(data.labelId, this.instanceId, contact.jid);
|
|
|
|
return { numberJid: contact.jid, labelId: data.labelId, remove: true };
|
|
}
|
|
} catch (error) {
|
|
throw new BadRequestException(`Unable to ${data.action} label to chat`, error.toString());
|
|
}
|
|
}
|
|
|
|
// Group
|
|
private async updateGroupMetadataCache(groupJid: string) {
|
|
try {
|
|
const meta = await this.client.groupMetadata(groupJid);
|
|
|
|
const cacheConf = this.configService.get<CacheConf>('CACHE');
|
|
|
|
if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) {
|
|
this.logger.verbose(`Updating cache for group: ${groupJid}`);
|
|
await groupMetadataCache.set(groupJid, {
|
|
timestamp: Date.now(),
|
|
data: meta,
|
|
});
|
|
}
|
|
|
|
return meta;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private getGroupMetadataCache = async (groupJid: string) => {
|
|
if (!isJidGroup(groupJid)) return null;
|
|
|
|
const cacheConf = this.configService.get<CacheConf>('CACHE');
|
|
|
|
if ((cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') || cacheConf?.LOCAL?.ENABLED) {
|
|
if (await groupMetadataCache?.has(groupJid)) {
|
|
console.log(`Cache request for group: ${groupJid}`);
|
|
const meta = await groupMetadataCache.get(groupJid);
|
|
|
|
if (Date.now() - meta.timestamp > 3600000) {
|
|
await this.updateGroupMetadataCache(groupJid);
|
|
}
|
|
|
|
return meta.data;
|
|
}
|
|
|
|
console.log(`Cache request for group: ${groupJid} - not found`);
|
|
return await this.updateGroupMetadataCache(groupJid);
|
|
}
|
|
|
|
return await this.findGroup({ groupJid }, 'inner');
|
|
};
|
|
|
|
public async createGroup(create: CreateGroupDto) {
|
|
try {
|
|
const participants = (await this.whatsappNumber({ numbers: create.participants }))
|
|
.filter((participant) => participant.exists)
|
|
.map((participant) => participant.jid);
|
|
const { id } = await this.client.groupCreate(create.subject, participants);
|
|
|
|
if (create?.description) {
|
|
await this.client.groupUpdateDescription(id, create.description);
|
|
}
|
|
|
|
if (create?.promoteParticipants) {
|
|
await this.updateGParticipant({
|
|
groupJid: id,
|
|
action: 'promote',
|
|
participants: participants,
|
|
});
|
|
}
|
|
|
|
const group = await this.client.groupMetadata(id);
|
|
|
|
return group;
|
|
} catch (error) {
|
|
this.logger.error(error);
|
|
throw new InternalServerErrorException('Error creating group', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateGroupPicture(picture: GroupPictureDto) {
|
|
try {
|
|
let pic: WAMediaUpload;
|
|
if (isURL(picture.image)) {
|
|
const timestamp = new Date().getTime();
|
|
const url = `${picture.image}?timestamp=${timestamp}`;
|
|
|
|
let config: any = {
|
|
responseType: 'arraybuffer',
|
|
};
|
|
|
|
if (this.localProxy?.enabled) {
|
|
config = {
|
|
...config,
|
|
httpsAgent: makeProxyAgent({
|
|
host: this.localProxy.host,
|
|
port: this.localProxy.port,
|
|
protocol: this.localProxy.protocol,
|
|
username: this.localProxy.username,
|
|
password: this.localProxy.password,
|
|
}),
|
|
};
|
|
}
|
|
|
|
pic = (await axios.get(url, config)).data;
|
|
} else if (isBase64(picture.image)) {
|
|
pic = Buffer.from(picture.image, 'base64');
|
|
} else {
|
|
throw new BadRequestException('"profilePicture" must be a url or a base64');
|
|
}
|
|
await this.client.updateProfilePicture(picture.groupJid, pic);
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error update group picture', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateGroupSubject(data: GroupSubjectDto) {
|
|
try {
|
|
await this.client.groupUpdateSubject(data.groupJid, data.subject);
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating group subject', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateGroupDescription(data: GroupDescriptionDto) {
|
|
try {
|
|
await this.client.groupUpdateDescription(data.groupJid, data.description);
|
|
|
|
return { update: 'success' };
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error updating group description', error.toString());
|
|
}
|
|
}
|
|
|
|
public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') {
|
|
try {
|
|
const group = await this.client.groupMetadata(id.groupJid);
|
|
|
|
if (!group) {
|
|
this.logger.error('Group not found');
|
|
return null;
|
|
}
|
|
|
|
const picture = await this.profilePicture(group.id);
|
|
|
|
return {
|
|
id: group.id,
|
|
subject: group.subject,
|
|
subjectOwner: group.subjectOwner,
|
|
subjectTime: group.subjectTime,
|
|
pictureUrl: picture.profilePictureUrl,
|
|
size: group.participants.length,
|
|
creation: group.creation,
|
|
owner: group.owner,
|
|
desc: group.desc,
|
|
descId: group.descId,
|
|
restrict: group.restrict,
|
|
announce: group.announce,
|
|
participants: group.participants,
|
|
isCommunity: group.isCommunity,
|
|
isCommunityAnnounce: group.isCommunityAnnounce,
|
|
linkedParent: group.linkedParent,
|
|
};
|
|
} catch (error) {
|
|
if (reply === 'inner') {
|
|
return;
|
|
}
|
|
throw new NotFoundException('Error fetching group', error.toString());
|
|
}
|
|
}
|
|
|
|
public async fetchAllGroups(getParticipants: GetParticipant) {
|
|
const fetch = Object.values(await this?.client?.groupFetchAllParticipating());
|
|
|
|
let groups = [];
|
|
for (const group of fetch) {
|
|
const picture = await this.profilePicture(group.id);
|
|
|
|
const result = {
|
|
id: group.id,
|
|
subject: group.subject,
|
|
subjectOwner: group.subjectOwner,
|
|
subjectTime: group.subjectTime,
|
|
pictureUrl: picture?.profilePictureUrl,
|
|
size: group.participants.length,
|
|
creation: group.creation,
|
|
owner: group.owner,
|
|
desc: group.desc,
|
|
descId: group.descId,
|
|
restrict: group.restrict,
|
|
announce: group.announce,
|
|
isCommunity: group.isCommunity,
|
|
isCommunityAnnounce: group.isCommunityAnnounce,
|
|
linkedParent: group.linkedParent,
|
|
};
|
|
|
|
if (getParticipants.getParticipants == 'true') {
|
|
result['participants'] = group.participants;
|
|
}
|
|
|
|
groups = [...groups, result];
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
public async inviteCode(id: GroupJid) {
|
|
try {
|
|
const code = await this.client.groupInviteCode(id.groupJid);
|
|
return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code };
|
|
} catch (error) {
|
|
throw new NotFoundException('No invite code', error.toString());
|
|
}
|
|
}
|
|
|
|
public async inviteInfo(id: GroupInvite) {
|
|
try {
|
|
return await this.client.groupGetInviteInfo(id.inviteCode);
|
|
} catch (error) {
|
|
throw new NotFoundException('No invite info', id.inviteCode);
|
|
}
|
|
}
|
|
|
|
public async sendInvite(id: GroupSendInvite) {
|
|
try {
|
|
const inviteCode = await this.inviteCode({ groupJid: id.groupJid });
|
|
|
|
const inviteUrl = inviteCode.inviteUrl;
|
|
|
|
const numbers = id.numbers.map((number) => createJid(number));
|
|
const description = id.description ?? '';
|
|
|
|
const msg = `${description}\n\n${inviteUrl}`;
|
|
|
|
const message = {
|
|
conversation: msg,
|
|
};
|
|
|
|
for await (const number of numbers) {
|
|
await this.sendMessageWithTyping(number, message);
|
|
}
|
|
|
|
return { send: true, inviteUrl };
|
|
} catch (error) {
|
|
throw new NotFoundException('No send invite');
|
|
}
|
|
}
|
|
|
|
public async acceptInviteCode(id: AcceptGroupInvite) {
|
|
try {
|
|
const groupJid = await this.client.groupAcceptInvite(id.inviteCode);
|
|
return { accepted: true, groupJid: groupJid };
|
|
} catch (error) {
|
|
throw new NotFoundException('Accept invite error', error.toString());
|
|
}
|
|
}
|
|
|
|
public async revokeInviteCode(id: GroupJid) {
|
|
try {
|
|
const inviteCode = await this.client.groupRevokeInvite(id.groupJid);
|
|
return { revoked: true, inviteCode };
|
|
} catch (error) {
|
|
throw new NotFoundException('Revoke error', error.toString());
|
|
}
|
|
}
|
|
|
|
public async findParticipants(id: GroupJid) {
|
|
try {
|
|
const participants = (await this.client.groupMetadata(id.groupJid)).participants;
|
|
const contacts = await this.prismaRepository.contact.findMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
remoteJid: {
|
|
in: participants.map((p) => p.id),
|
|
},
|
|
},
|
|
});
|
|
const parsedParticipants = participants.map((participant) => {
|
|
const contact = contacts.find((c) => c.remoteJid === participant.id);
|
|
return {
|
|
...participant,
|
|
name: participant.name ?? contact?.pushName,
|
|
imgUrl: participant.imgUrl ?? contact?.profilePicUrl,
|
|
};
|
|
});
|
|
|
|
const usersContacts = parsedParticipants.filter((c) => c.id.includes('@s.whatsapp'));
|
|
if (usersContacts) {
|
|
await saveOnWhatsappCache(usersContacts.map((c) => ({ remoteJid: c.id })));
|
|
}
|
|
|
|
return { participants: parsedParticipants };
|
|
} catch (error) {
|
|
console.error(error);
|
|
throw new NotFoundException('No participants', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateGParticipant(update: GroupUpdateParticipantDto) {
|
|
try {
|
|
const participants = update.participants.map((p) => createJid(p));
|
|
const updateParticipants = await this.client.groupParticipantsUpdate(
|
|
update.groupJid,
|
|
participants,
|
|
update.action,
|
|
);
|
|
return { updateParticipants: updateParticipants };
|
|
} catch (error) {
|
|
throw new BadRequestException('Error updating participants', error.toString());
|
|
}
|
|
}
|
|
|
|
public async updateGSetting(update: GroupUpdateSettingDto) {
|
|
try {
|
|
const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action);
|
|
return { updateSetting: updateSetting };
|
|
} catch (error) {
|
|
throw new BadRequestException('Error updating setting', error.toString());
|
|
}
|
|
}
|
|
|
|
public async toggleEphemeral(update: GroupToggleEphemeralDto) {
|
|
try {
|
|
await this.client.groupToggleEphemeral(update.groupJid, update.expiration);
|
|
return { success: true };
|
|
} catch (error) {
|
|
throw new BadRequestException('Error updating setting', error.toString());
|
|
}
|
|
}
|
|
|
|
public async leaveGroup(id: GroupJid) {
|
|
try {
|
|
await this.client.groupLeave(id.groupJid);
|
|
return { groupJid: id.groupJid, leave: true };
|
|
} catch (error) {
|
|
throw new BadRequestException('Unable to leave the group', error.toString());
|
|
}
|
|
}
|
|
|
|
public async templateMessage() {
|
|
throw new Error('Method not available in the Baileys service');
|
|
}
|
|
|
|
private prepareMessage(message: proto.IWebMessageInfo): any {
|
|
const contentType = getContentType(message.message);
|
|
const contentMsg = message?.message[contentType] as any;
|
|
|
|
const messageRaw = {
|
|
key: message.key,
|
|
pushName:
|
|
message.pushName ||
|
|
(message.key.fromMe
|
|
? 'Você'
|
|
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
|
status: status[message.status],
|
|
message: { ...message.message },
|
|
contextInfo: contentMsg?.contextInfo,
|
|
messageType: contentType || 'unknown',
|
|
messageTimestamp: message.messageTimestamp as number,
|
|
instanceId: this.instanceId,
|
|
source: getDevice(message.key.id),
|
|
};
|
|
|
|
if (!messageRaw.status && message.key.fromMe === false) {
|
|
messageRaw.status = status[3]; // DELIVERED MESSAGE
|
|
}
|
|
|
|
if (messageRaw.message.extendedTextMessage) {
|
|
messageRaw.messageType = 'conversation';
|
|
messageRaw.message.conversation = messageRaw.message.extendedTextMessage.text;
|
|
delete messageRaw.message.extendedTextMessage;
|
|
}
|
|
|
|
if (messageRaw.message.documentWithCaptionMessage) {
|
|
messageRaw.messageType = 'documentMessage';
|
|
messageRaw.message.documentMessage = messageRaw.message.documentWithCaptionMessage.message.documentMessage;
|
|
delete messageRaw.message.documentWithCaptionMessage;
|
|
}
|
|
|
|
const quotedMessage = messageRaw?.contextInfo?.quotedMessage;
|
|
if (quotedMessage) {
|
|
if (quotedMessage.extendedTextMessage) {
|
|
quotedMessage.conversation = quotedMessage.extendedTextMessage.text;
|
|
delete quotedMessage.extendedTextMessage;
|
|
}
|
|
|
|
if (quotedMessage.documentWithCaptionMessage) {
|
|
quotedMessage.documentMessage = quotedMessage.documentWithCaptionMessage.message.documentMessage;
|
|
delete quotedMessage.documentWithCaptionMessage;
|
|
}
|
|
}
|
|
|
|
return messageRaw;
|
|
}
|
|
|
|
private async syncChatwootLostMessages() {
|
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
|
const chatwootConfig = await this.findChatwoot();
|
|
const prepare = (message: any) => this.prepareMessage(message);
|
|
this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare);
|
|
|
|
const task = cron.schedule('0,30 * * * *', async () => {
|
|
this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare);
|
|
});
|
|
task.start();
|
|
}
|
|
}
|
|
|
|
private async updateMessagesReadedByTimestamp(remoteJid: string, timestamp?: number): Promise<number> {
|
|
if (timestamp === undefined || timestamp === null) return 0;
|
|
|
|
const result = await this.prismaRepository.message.updateMany({
|
|
where: {
|
|
AND: [
|
|
{ key: { path: ['remoteJid'], equals: remoteJid } },
|
|
{ key: { path: ['fromMe'], equals: false } },
|
|
{ messageTimestamp: { lte: timestamp } },
|
|
{
|
|
OR: [{ status: null }, { status: status[3] }],
|
|
},
|
|
],
|
|
},
|
|
data: { status: status[4] },
|
|
});
|
|
|
|
if (result) {
|
|
if (result.count > 0) {
|
|
this.updateChatUnreadMessages(remoteJid);
|
|
}
|
|
|
|
return result.count;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private async updateChatUnreadMessages(remoteJid: string): Promise<number> {
|
|
const [chat, unreadMessages] = await Promise.all([
|
|
this.prismaRepository.chat.findFirst({ where: { remoteJid } }),
|
|
this.prismaRepository.message.count({
|
|
where: {
|
|
AND: [
|
|
{ key: { path: ['remoteJid'], equals: remoteJid } },
|
|
{ key: { path: ['fromMe'], equals: false } },
|
|
{ status: { equals: status[3] } },
|
|
],
|
|
},
|
|
}),
|
|
]);
|
|
|
|
if (chat && chat.unreadMessages !== unreadMessages) {
|
|
await this.prismaRepository.chat.update({
|
|
where: { id: chat.id },
|
|
data: { unreadMessages },
|
|
});
|
|
}
|
|
|
|
return unreadMessages;
|
|
}
|
|
|
|
private async addLabel(labelId: string, instanceId: string, chatId: string) {
|
|
const id = cuid();
|
|
|
|
await this.prismaRepository.$executeRawUnsafe(
|
|
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
|
|
VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
|
|
DO
|
|
UPDATE
|
|
SET "labels" = (
|
|
SELECT to_jsonb(array_agg(DISTINCT elem))
|
|
FROM (
|
|
SELECT jsonb_array_elements_text("Chat"."labels") AS elem
|
|
UNION
|
|
SELECT $1::text AS elem
|
|
) sub
|
|
),
|
|
"updatedAt" = NOW();`,
|
|
labelId,
|
|
instanceId,
|
|
chatId,
|
|
id,
|
|
);
|
|
}
|
|
|
|
private async removeLabel(labelId: string, instanceId: string, chatId: string) {
|
|
const id = cuid();
|
|
|
|
await this.prismaRepository.$executeRawUnsafe(
|
|
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
|
|
VALUES ($4, $2, $3, '[]'::jsonb, NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
|
|
DO
|
|
UPDATE
|
|
SET "labels" = COALESCE (
|
|
(
|
|
SELECT jsonb_agg(elem)
|
|
FROM jsonb_array_elements_text("Chat"."labels") AS elem
|
|
WHERE elem <> $1
|
|
),
|
|
'[]'::jsonb
|
|
),
|
|
"updatedAt" = NOW();`,
|
|
labelId,
|
|
instanceId,
|
|
chatId,
|
|
id,
|
|
);
|
|
}
|
|
|
|
public async baileysOnWhatsapp(jid: string) {
|
|
const response = await this.client.onWhatsApp(jid);
|
|
|
|
return response;
|
|
}
|
|
|
|
public async baileysProfilePictureUrl(jid: string, type: 'image' | 'preview', timeoutMs: number) {
|
|
const response = await this.client.profilePictureUrl(jid, type, timeoutMs);
|
|
|
|
return response;
|
|
}
|
|
|
|
public async baileysAssertSessions(jids: string[], force: boolean) {
|
|
const response = await this.client.assertSessions(jids, force);
|
|
|
|
return response;
|
|
}
|
|
|
|
public async baileysCreateParticipantNodes(jids: string[], message: proto.IMessage, extraAttrs: any) {
|
|
const response = await this.client.createParticipantNodes(jids, message, extraAttrs);
|
|
|
|
const convertedResponse = {
|
|
...response,
|
|
nodes: response.nodes.map((node: any) => ({
|
|
...node,
|
|
content: node.content?.map((c: any) => ({
|
|
...c,
|
|
content: c.content instanceof Uint8Array ? Buffer.from(c.content).toString('base64') : c.content,
|
|
})),
|
|
})),
|
|
};
|
|
|
|
return convertedResponse;
|
|
}
|
|
|
|
public async baileysSendNode(stanza: any) {
|
|
console.log('stanza', JSON.stringify(stanza));
|
|
const response = await this.client.sendNode(stanza);
|
|
|
|
return response;
|
|
}
|
|
|
|
public async baileysGetUSyncDevices(jids: string[], useCache: boolean, ignoreZeroDevices: boolean) {
|
|
const response = await this.client.getUSyncDevices(jids, useCache, ignoreZeroDevices);
|
|
|
|
return response;
|
|
}
|
|
|
|
public async baileysGenerateMessageTag() {
|
|
const response = await this.client.generateMessageTag();
|
|
|
|
return response;
|
|
}
|
|
|
|
public async baileysSignalRepositoryDecryptMessage(jid: string, type: 'pkmsg' | 'msg', ciphertext: string) {
|
|
try {
|
|
const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
|
|
|
|
const response = await this.client.signalRepository.decryptMessage({
|
|
jid,
|
|
type,
|
|
ciphertext: ciphertextBuffer,
|
|
});
|
|
|
|
return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response;
|
|
} catch (error) {
|
|
this.logger.error('Error decrypting message:');
|
|
this.logger.error(error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public async baileysGetAuthState() {
|
|
const response = {
|
|
me: this.client.authState.creds.me,
|
|
account: this.client.authState.creds.account,
|
|
};
|
|
|
|
return response;
|
|
}
|
|
|
|
//Business Controller
|
|
public async fetchCatalog(instanceName: string, data: getCollectionsDto) {
|
|
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
|
|
const limit = data.limit || 10;
|
|
const cursor = null;
|
|
|
|
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
|
|
|
if (!onWhatsapp.exists) {
|
|
throw new BadRequestException(onWhatsapp);
|
|
}
|
|
|
|
try {
|
|
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
|
const business = await this.fetchBusinessProfile(info?.jid);
|
|
|
|
let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor });
|
|
let nextPageCursor = catalog.nextPageCursor;
|
|
let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
|
|
let pagination = nextPageCursorJson?.pagination_cursor
|
|
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
|
|
: null;
|
|
let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
|
|
|
|
let productsCatalog = catalog.products || [];
|
|
let countLoops = 0;
|
|
while (fetcherHasMore && countLoops < 4) {
|
|
catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor });
|
|
nextPageCursor = catalog.nextPageCursor;
|
|
nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
|
|
pagination = nextPageCursorJson?.pagination_cursor
|
|
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
|
|
: null;
|
|
fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
|
|
productsCatalog = [...productsCatalog, ...catalog.products];
|
|
countLoops++;
|
|
}
|
|
|
|
return {
|
|
wuid: info?.jid || jid,
|
|
numberExists: info?.exists,
|
|
isBusiness: business.isBusiness,
|
|
catalogLength: productsCatalog.length,
|
|
catalog: productsCatalog,
|
|
};
|
|
} catch (error) {
|
|
console.log(error);
|
|
return {
|
|
wuid: jid,
|
|
name: null,
|
|
isBusiness: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
public async getCatalog({
|
|
jid,
|
|
limit,
|
|
cursor,
|
|
}: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> {
|
|
try {
|
|
jid = jid ? createJid(jid) : this.instance.wuid;
|
|
|
|
const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor });
|
|
|
|
if (!catalog) {
|
|
return {
|
|
products: undefined,
|
|
nextPageCursor: undefined,
|
|
};
|
|
}
|
|
|
|
return catalog;
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error getCatalog', error.toString());
|
|
}
|
|
}
|
|
|
|
public async fetchCollections(instanceName: string, data: getCollectionsDto) {
|
|
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
|
|
const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque)
|
|
|
|
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
|
|
|
if (!onWhatsapp.exists) {
|
|
throw new BadRequestException(onWhatsapp);
|
|
}
|
|
|
|
try {
|
|
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
|
|
const business = await this.fetchBusinessProfile(info?.jid);
|
|
const collections = await this.getCollections(info?.jid, limit);
|
|
|
|
return {
|
|
wuid: info?.jid || jid,
|
|
name: info?.name,
|
|
numberExists: info?.exists,
|
|
isBusiness: business.isBusiness,
|
|
collectionsLength: collections?.length,
|
|
collections: collections,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
wuid: jid,
|
|
name: null,
|
|
isBusiness: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
public async getCollections(jid?: string | undefined, limit?: number): Promise<CatalogCollection[]> {
|
|
try {
|
|
jid = jid ? createJid(jid) : this.instance.wuid;
|
|
|
|
const result = await this.client.getCollections(jid, limit);
|
|
|
|
if (!result) {
|
|
return [
|
|
{
|
|
id: undefined,
|
|
name: undefined,
|
|
products: [],
|
|
status: undefined,
|
|
},
|
|
];
|
|
}
|
|
|
|
return result.collections;
|
|
} catch (error) {
|
|
throw new InternalServerErrorException('Error getCatalog', error.toString());
|
|
}
|
|
}
|
|
|
|
public async fetchMessages(query: Query<Message>) {
|
|
const keyFilters = query?.where?.key as {
|
|
id?: string;
|
|
fromMe?: boolean;
|
|
remoteJid?: string;
|
|
participants?: string;
|
|
};
|
|
|
|
const timestampFilter = {};
|
|
if (query?.where?.messageTimestamp) {
|
|
if (query.where.messageTimestamp['gte'] && query.where.messageTimestamp['lte']) {
|
|
timestampFilter['messageTimestamp'] = {
|
|
gte: Math.floor(new Date(query.where.messageTimestamp['gte']).getTime() / 1000),
|
|
lte: Math.floor(new Date(query.where.messageTimestamp['lte']).getTime() / 1000),
|
|
};
|
|
}
|
|
}
|
|
|
|
const count = await this.prismaRepository.message.count({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
id: query?.where?.id,
|
|
source: query?.where?.source,
|
|
messageType: query?.where?.messageType,
|
|
...timestampFilter,
|
|
AND: [
|
|
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
|
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
|
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
|
keyFilters?.participants ? { key: { path: ['participants'], equals: keyFilters?.participants } } : {},
|
|
],
|
|
},
|
|
});
|
|
|
|
if (!query?.offset) {
|
|
query.offset = 50;
|
|
}
|
|
|
|
if (!query?.page) {
|
|
query.page = 1;
|
|
}
|
|
|
|
const messages = await this.prismaRepository.message.findMany({
|
|
where: {
|
|
instanceId: this.instanceId,
|
|
id: query?.where?.id,
|
|
source: query?.where?.source,
|
|
messageType: query?.where?.messageType,
|
|
...timestampFilter,
|
|
AND: [
|
|
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
|
|
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
|
|
keyFilters?.remoteJid ? { key: { path: ['remoteJid'], equals: keyFilters?.remoteJid } } : {},
|
|
keyFilters?.participants ? { key: { path: ['participants'], equals: keyFilters?.participants } } : {},
|
|
],
|
|
},
|
|
orderBy: {
|
|
messageTimestamp: 'desc',
|
|
},
|
|
skip: query.offset * (query?.page === 1 ? 0 : (query?.page as number) - 1),
|
|
take: query.offset,
|
|
select: {
|
|
id: true,
|
|
key: true,
|
|
pushName: true,
|
|
messageType: true,
|
|
message: true,
|
|
messageTimestamp: true,
|
|
instanceId: true,
|
|
source: true,
|
|
contextInfo: true,
|
|
MessageUpdate: {
|
|
select: {
|
|
status: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const formattedMessages = messages.map((message) => {
|
|
const messageKey = message.key as { fromMe: boolean; remoteJid: string; id: string; participant?: string };
|
|
|
|
if (!message.pushName) {
|
|
if (messageKey.fromMe) {
|
|
message.pushName = 'Você';
|
|
} else if (message.contextInfo) {
|
|
const contextInfo = message.contextInfo as { participant?: string };
|
|
if (contextInfo.participant) {
|
|
message.pushName = contextInfo.participant.split('@')[0];
|
|
} else if (messageKey.participant) {
|
|
message.pushName = messageKey.participant.split('@')[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
return message;
|
|
});
|
|
|
|
return {
|
|
messages: {
|
|
total: count,
|
|
pages: Math.ceil(count / query.offset),
|
|
currentPage: query.page,
|
|
records: formattedMessages,
|
|
},
|
|
};
|
|
}
|
|
}
|