mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-27 07:37:44 -06:00
Compare commits
13 Commits
6ede76f8cc
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6efa879081 | ||
|
|
2534ec2307 | ||
|
|
933a28de26 | ||
|
|
b1b07b7e7f | ||
|
|
bb831d590f | ||
|
|
cb41e65e29 | ||
|
|
52a8d9ea71 | ||
|
|
c7b7a9992e | ||
|
|
f46699ef3f | ||
|
|
72b0833ce2 | ||
|
|
2e3c8184ef | ||
|
|
6f2bef678c | ||
|
|
3325044500 |
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Instance` MODIFY `token` VARCHAR(500);
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ model Instance {
|
|||||||
integration String? @db.VarChar(100)
|
integration String? @db.VarChar(100)
|
||||||
number String? @db.VarChar(100)
|
number String? @db.VarChar(100)
|
||||||
businessId String? @db.VarChar(100)
|
businessId String? @db.VarChar(100)
|
||||||
token String? @db.VarChar(255)
|
token String? @db.VarChar(500)
|
||||||
clientName String? @db.VarChar(100)
|
clientName String? @db.VarChar(100)
|
||||||
disconnectionReasonCode Int? @db.Int
|
disconnectionReasonCode Int? @db.Int
|
||||||
disconnectionObject Json? @db.Json
|
disconnectionObject Json? @db.Json
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Instance" ALTER COLUMN "token" TYPE VARCHAR(500);
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ model Instance {
|
|||||||
integration String? @db.VarChar(100)
|
integration String? @db.VarChar(100)
|
||||||
number String? @db.VarChar(100)
|
number String? @db.VarChar(100)
|
||||||
businessId String? @db.VarChar(100)
|
businessId String? @db.VarChar(100)
|
||||||
token String? @db.VarChar(255)
|
token String? @db.VarChar(500)
|
||||||
clientName String? @db.VarChar(100)
|
clientName String? @db.VarChar(100)
|
||||||
disconnectionReasonCode Int? @db.Integer
|
disconnectionReasonCode Int? @db.Integer
|
||||||
disconnectionObject Json? @db.JsonB
|
disconnectionObject Json? @db.JsonB
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ model Instance {
|
|||||||
integration String? @db.VarChar(100)
|
integration String? @db.VarChar(100)
|
||||||
number String? @db.VarChar(100)
|
number String? @db.VarChar(100)
|
||||||
businessId String? @db.VarChar(100)
|
businessId String? @db.VarChar(100)
|
||||||
token String? @db.VarChar(255)
|
token String? @db.VarChar(500)
|
||||||
clientName String? @db.VarChar(100)
|
clientName String? @db.VarChar(100)
|
||||||
disconnectionReasonCode Int? @db.Integer
|
disconnectionReasonCode Int? @db.Integer
|
||||||
disconnectionObject Json? @db.JsonB
|
disconnectionObject Json? @db.JsonB
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export class ChatController {
|
|||||||
remoteJid: data.remoteJid,
|
remoteJid: data.remoteJid,
|
||||||
};
|
};
|
||||||
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey);
|
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey);
|
||||||
|
}
|
||||||
|
|
||||||
public async fetchChannels({ instanceName }: InstanceDto, query: Query<Contact>) {
|
public async fetchChannels({ instanceName }: InstanceDto, query: Query<Contact>) {
|
||||||
return await this.waMonitor.waInstances[instanceName].fetchChannels(query);
|
return await this.waMonitor.waInstances[instanceName].fetchChannels(query);
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
|
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
|
||||||
private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false });
|
private readonly userDevicesCache: CacheStore = new NodeCache({ stdTTL: 300000, useClones: false });
|
||||||
private endSession = false;
|
private endSession = false;
|
||||||
|
private isDeleting = false; // Flag to prevent reconnection during deletion
|
||||||
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
||||||
private eventProcessingQueue: Promise<void> = Promise.resolve();
|
private eventProcessingQueue: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
@@ -265,10 +266,27 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async logoutInstance() {
|
public async logoutInstance() {
|
||||||
this.messageProcessor.onDestroy();
|
// Mark instance as deleting to prevent reconnection attempts
|
||||||
await this.client?.logout('Log out instance: ' + this.instanceName);
|
this.isDeleting = true;
|
||||||
|
this.endSession = true;
|
||||||
|
|
||||||
this.client?.ws?.close();
|
this.messageProcessor.onDestroy();
|
||||||
|
|
||||||
|
if (this.client) {
|
||||||
|
try {
|
||||||
|
await this.client.logout('Log out instance: ' + this.instanceName);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({ message: 'Error during logout', error });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Improved socket cleanup
|
||||||
|
try {
|
||||||
|
this.client.ws?.close();
|
||||||
|
this.client.end(new Error('Instance logout'));
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({ message: 'Error during socket cleanup', error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const db = this.configService.get<Database>('DATABASE');
|
const db = this.configService.get<Database>('DATABASE');
|
||||||
const cache = this.configService.get<CacheConf>('CACHE');
|
const cache = this.configService.get<CacheConf>('CACHE');
|
||||||
@@ -332,6 +350,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async connectionUpdate({ qr, connection, lastDisconnect }: Partial<ConnectionState>) {
|
private async connectionUpdate({ qr, connection, lastDisconnect }: Partial<ConnectionState>) {
|
||||||
|
// Enhanced logging for connection updates
|
||||||
|
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
||||||
|
this.logger.info({
|
||||||
|
message: 'Connection update received',
|
||||||
|
connection,
|
||||||
|
hasQr: !!qr,
|
||||||
|
statusCode,
|
||||||
|
instanceName: this.instance.name,
|
||||||
|
isDeleting: this.isDeleting,
|
||||||
|
endSession: this.endSession,
|
||||||
|
});
|
||||||
|
|
||||||
if (qr) {
|
if (qr) {
|
||||||
if (this.instance.qrcode.count === this.configService.get<QrCode>('QRCODE').LIMIT) {
|
if (this.instance.qrcode.count === this.configService.get<QrCode>('QRCODE').LIMIT) {
|
||||||
this.sendDataWebhook(Events.QRCODE_UPDATED, {
|
this.sendDataWebhook(Events.QRCODE_UPDATED, {
|
||||||
@@ -424,11 +454,29 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (connection === 'close') {
|
if (connection === 'close') {
|
||||||
|
// Check if instance is being deleted or session is ending
|
||||||
|
if (this.isDeleting || this.endSession) {
|
||||||
|
this.logger.info('Instance is being deleted/ended, skipping reconnection attempt');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
||||||
const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406];
|
const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406];
|
||||||
const shouldReconnect = !codesToNotReconnect.includes(statusCode);
|
const shouldReconnect = !codesToNotReconnect.includes(statusCode);
|
||||||
|
|
||||||
|
this.logger.info({
|
||||||
|
message: 'Connection closed, evaluating reconnection',
|
||||||
|
statusCode,
|
||||||
|
shouldReconnect,
|
||||||
|
instanceName: this.instance.name,
|
||||||
|
});
|
||||||
|
|
||||||
if (shouldReconnect) {
|
if (shouldReconnect) {
|
||||||
await this.connectToWhatsapp(this.phoneNumber);
|
// Add 3 second delay before reconnection to prevent rapid reconnection loops
|
||||||
|
this.logger.info('Reconnecting in 3 seconds...');
|
||||||
|
setTimeout(async () => {
|
||||||
|
await this.connectToWhatsapp(this.phoneNumber);
|
||||||
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
this.sendDataWebhook(Events.STATUS_INSTANCE, {
|
this.sendDataWebhook(Events.STATUS_INSTANCE, {
|
||||||
instance: this.instance.name,
|
instance: this.instance.name,
|
||||||
@@ -591,10 +639,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
this.logger.info(`Browser: ${browser}`);
|
this.logger.info(`Browser: ${browser}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch latest WhatsApp Web version automatically
|
||||||
const baileysVersion = await fetchLatestWaWebVersion({});
|
const baileysVersion = await fetchLatestWaWebVersion({});
|
||||||
const version = baileysVersion.version;
|
const version = baileysVersion.version;
|
||||||
const log = `Baileys version: ${version.join('.')}`;
|
|
||||||
|
|
||||||
|
const log = `Baileys version: ${version.join('.')}`;
|
||||||
this.logger.info(log);
|
this.logger.info(log);
|
||||||
|
|
||||||
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
|
this.logger.info(`Group Ignore: ${this.localSettings.groupsIgnore}`);
|
||||||
@@ -602,7 +651,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
let options;
|
let options;
|
||||||
|
|
||||||
if (this.localProxy?.enabled) {
|
if (this.localProxy?.enabled) {
|
||||||
this.logger.info('Proxy enabled: ' + this.localProxy?.host);
|
this.logger.verbose('Proxy enabled');
|
||||||
|
|
||||||
if (this.localProxy?.host?.includes('proxyscrape')) {
|
if (this.localProxy?.host?.includes('proxyscrape')) {
|
||||||
try {
|
try {
|
||||||
@@ -611,9 +660,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
const proxyUrls = text.split('\r\n');
|
const proxyUrls = text.split('\r\n');
|
||||||
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
const rand = Math.floor(Math.random() * Math.floor(proxyUrls.length));
|
||||||
const proxyUrl = 'http://' + proxyUrls[rand];
|
const proxyUrl = 'http://' + proxyUrls[rand];
|
||||||
|
this.logger.info('Proxy url: ' + proxyUrl);
|
||||||
options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) };
|
options = { agent: makeProxyAgent(proxyUrl), fetchAgent: makeProxyAgentUndici(proxyUrl) };
|
||||||
} catch {
|
} catch (error) {
|
||||||
this.localProxy.enabled = false;
|
this.logger.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
options = {
|
options = {
|
||||||
@@ -1201,10 +1251,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageRaw = this.prepareMessage(received);
|
const messageRaw = this.prepareMessage(received) as any;
|
||||||
|
|
||||||
if (messageRaw.messageType === 'pollUpdateMessage') {
|
if (messageRaw.messageType === 'pollUpdateMessage') {
|
||||||
const pollCreationKey = messageRaw.message.pollUpdateMessage.pollCreationMessageKey;
|
const pollCreationKey = (messageRaw.message as any).pollUpdateMessage.pollCreationMessageKey;
|
||||||
const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo;
|
const pollMessage = (await this.getMessage(pollCreationKey, true)) as proto.IWebMessageInfo;
|
||||||
const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any;
|
const pollMessageSecret = (await this.getMessage(pollCreationKey)) as any;
|
||||||
|
|
||||||
@@ -1213,7 +1263,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
(pollMessage.message as any).pollCreationMessage?.options ||
|
(pollMessage.message as any).pollCreationMessage?.options ||
|
||||||
(pollMessage.message as any).pollCreationMessageV3?.options ||
|
(pollMessage.message as any).pollCreationMessageV3?.options ||
|
||||||
[];
|
[];
|
||||||
const pollVote = messageRaw.message.pollUpdateMessage.vote;
|
const pollVote = (messageRaw.message as any).pollUpdateMessage.vote;
|
||||||
|
|
||||||
const voterJid = received.key.fromMe
|
const voterJid = received.key.fromMe
|
||||||
? this.instance.wuid
|
? this.instance.wuid
|
||||||
@@ -1293,14 +1343,14 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
})
|
})
|
||||||
.map((option) => option.optionName);
|
.map((option) => option.optionName);
|
||||||
|
|
||||||
messageRaw.message.pollUpdateMessage.vote.selectedOptions = selectedOptionNames;
|
(messageRaw.message as any).pollUpdateMessage.vote.selectedOptions = selectedOptionNames;
|
||||||
|
|
||||||
const pollUpdates = pollOptions.map((option) => ({
|
const pollUpdates = pollOptions.map((option) => ({
|
||||||
name: option.optionName,
|
name: option.optionName,
|
||||||
voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [],
|
voters: selectedOptionNames.includes(option.optionName) ? [successfulVoterJid] : [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
messageRaw.pollUpdates = pollUpdates;
|
(messageRaw as any).pollUpdates = pollUpdates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1348,13 +1398,14 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
||||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(received, this)}`;
|
(messageRaw.message as any).speechToText =
|
||||||
|
`[audio] ${await this.openaiService.speechToText(received, this)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { pollUpdates, ...messageData } = messageRaw;
|
const { pollUpdates, ...messageData } = messageRaw as any;
|
||||||
const msg = await this.prismaRepository.message.create({ data: messageData });
|
const msg = await this.prismaRepository.message.create({ data: messageData });
|
||||||
|
|
||||||
const { remoteJid } = received.key;
|
const { remoteJid } = received.key;
|
||||||
@@ -1430,7 +1481,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||||
|
|
||||||
messageRaw.message.mediaUrl = mediaUrl;
|
(messageRaw.message as any).mediaUrl = mediaUrl;
|
||||||
|
|
||||||
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
|
||||||
}
|
}
|
||||||
@@ -1452,7 +1503,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
messageRaw.message.base64 = buffer.toString('base64');
|
(messageRaw.message as any).base64 = buffer.toString('base64');
|
||||||
} else {
|
} else {
|
||||||
// retry to download media
|
// retry to download media
|
||||||
const buffer = await downloadMediaMessage(
|
const buffer = await downloadMediaMessage(
|
||||||
@@ -1463,7 +1514,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (buffer) {
|
if (buffer) {
|
||||||
messageRaw.message.base64 = buffer.toString('base64');
|
(messageRaw.message as any).base64 = buffer.toString('base64');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1475,8 +1526,8 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
this.logger.verbose(messageRaw);
|
this.logger.verbose(messageRaw);
|
||||||
|
|
||||||
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
sendTelemetry(`received.message.${messageRaw.messageType ?? 'unknown'}`);
|
||||||
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
|
if ((messageRaw.key as any).remoteJid?.includes('@lid') && (messageRaw.key as any).remoteJidAlt) {
|
||||||
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
|
(messageRaw.key as any).remoteJid = (messageRaw.key as any).remoteJidAlt;
|
||||||
}
|
}
|
||||||
console.log(messageRaw);
|
console.log(messageRaw);
|
||||||
|
|
||||||
@@ -1484,7 +1535,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
await chatbotController.emit({
|
await chatbotController.emit({
|
||||||
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
|
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
|
||||||
remoteJid: messageRaw.key.remoteJid,
|
remoteJid: (messageRaw.key as any).remoteJid,
|
||||||
msg: messageRaw,
|
msg: messageRaw,
|
||||||
pushName: messageRaw.pushName,
|
pushName: messageRaw.pushName,
|
||||||
});
|
});
|
||||||
@@ -1513,9 +1564,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
await saveOnWhatsappCache([
|
await saveOnWhatsappCache([
|
||||||
{
|
{
|
||||||
remoteJid:
|
remoteJid:
|
||||||
messageRaw.key.addressingMode === 'lid' ? messageRaw.key.remoteJidAlt : messageRaw.key.remoteJid,
|
(messageRaw.key as any).addressingMode === 'lid'
|
||||||
remoteJidAlt: messageRaw.key.remoteJidAlt,
|
? (messageRaw.key as any).remoteJidAlt
|
||||||
lid: messageRaw.key.addressingMode === 'lid' ? 'lid' : null,
|
: (messageRaw.key as any).remoteJid,
|
||||||
|
remoteJidAlt: (messageRaw.key as any).remoteJidAlt,
|
||||||
|
lid: (messageRaw.key as any).addressingMode === 'lid' ? 'lid' : null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1561,7 +1614,18 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
const readChatToUpdate: Record<string, true> = {}; // {remoteJid: true}
|
||||||
|
|
||||||
for await (const { key, update } of args) {
|
for await (const { key, update } of args) {
|
||||||
if (settings?.groupsIgnore && key.remoteJid?.includes('@g.us')) {
|
const keyAny = key as any;
|
||||||
|
if (keyAny.remoteJid) {
|
||||||
|
keyAny.remoteJid = keyAny.remoteJid.replace(/:.*$/, '');
|
||||||
|
}
|
||||||
|
if (keyAny.participant) {
|
||||||
|
keyAny.participant = keyAny.participant.replace(/:.*$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRemoteJid = keyAny.remoteJid;
|
||||||
|
const normalizedParticipant = keyAny.participant;
|
||||||
|
|
||||||
|
if (settings?.groupsIgnore && normalizedRemoteJid?.includes('@g.us')) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1612,9 +1676,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
const message: any = {
|
const message: any = {
|
||||||
keyId: key.id,
|
keyId: key.id,
|
||||||
remoteJid: key?.remoteJid,
|
remoteJid: normalizedRemoteJid,
|
||||||
fromMe: key.fromMe,
|
fromMe: key.fromMe,
|
||||||
participant: key?.participant,
|
participant: normalizedParticipant,
|
||||||
status: status[update.status] ?? 'SERVER_ACK',
|
status: status[update.status] ?? 'SERVER_ACK',
|
||||||
pollUpdates,
|
pollUpdates,
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
@@ -1637,18 +1701,48 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
const searchId = originalMessageId || key.id;
|
const searchId = originalMessageId || key.id;
|
||||||
|
|
||||||
const messages = (await this.prismaRepository.$queryRaw`
|
let retries = 0;
|
||||||
SELECT * FROM "Message"
|
const maxRetries = 3;
|
||||||
WHERE "instanceId" = ${this.instanceId}
|
const retryDelay = 500; // 500ms delay to avoid blocking for too long
|
||||||
AND "key"->>'id' = ${searchId}
|
|
||||||
LIMIT 1
|
while (retries < maxRetries) {
|
||||||
`) as any[];
|
const messages = (await this.prismaRepository.$queryRaw`
|
||||||
findMessage = messages[0] || null;
|
SELECT * FROM "Message"
|
||||||
|
WHERE "instanceId" = ${this.instanceId}
|
||||||
|
AND "key"->>'id' = ${searchId}
|
||||||
|
LIMIT 1
|
||||||
|
`) as any[];
|
||||||
|
findMessage = messages[0] || null;
|
||||||
|
|
||||||
|
if (findMessage?.id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
retries++;
|
||||||
|
if (retries < maxRetries) {
|
||||||
|
await delay(retryDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!findMessage?.id) {
|
if (!findMessage?.id) {
|
||||||
this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`);
|
this.logger.verbose(
|
||||||
|
`Original message not found for update after ${maxRetries} retries. Skipping. This is expected for protocol messages or ephemeral events not saved to the database. Key: ${JSON.stringify(key)}`,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync the incoming key.remoteJid with the stored one.
|
||||||
|
// This mutation is safe and necessary because Baileys events might use LIDs while we store Phone JIDs (or vice versa).
|
||||||
|
// Normalizing ensuring downstream logic uses the identifier that exists in our database.
|
||||||
|
if (findMessage?.key?.remoteJid && key.remoteJid !== findMessage.key.remoteJid) {
|
||||||
|
key.remoteJid = findMessage.key.remoteJid;
|
||||||
|
}
|
||||||
|
if (findMessage?.key?.remoteJid && findMessage.key.remoteJid !== key.remoteJid) {
|
||||||
|
this.logger.verbose(
|
||||||
|
`Updating key.remoteJid from ${key.remoteJid} to ${findMessage.key.remoteJid} based on stored message`,
|
||||||
|
);
|
||||||
|
key.remoteJid = findMessage.key.remoteJid;
|
||||||
|
}
|
||||||
message.messageId = findMessage.id;
|
message.messageId = findMessage.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2422,7 +2516,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber();
|
messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageRaw = this.prepareMessage(messageSent);
|
const messageRaw = this.prepareMessage(messageSent) as any;
|
||||||
|
|
||||||
const isMedia =
|
const isMedia =
|
||||||
messageSent?.message?.imageMessage ||
|
messageSent?.message?.imageMessage ||
|
||||||
@@ -2444,14 +2538,15 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.configService.get<Openai>('OPENAI').ENABLED && messageRaw?.message?.audioMessage) {
|
if (this.configService.get<Openai>('OPENAI').ENABLED && (messageRaw as any)?.message?.audioMessage) {
|
||||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||||
where: { instanceId: this.instanceId },
|
where: { instanceId: this.instanceId },
|
||||||
include: { OpenaiCreds: true },
|
include: { OpenaiCreds: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) {
|
||||||
messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText(messageRaw, this)}`;
|
(messageRaw.message as any).speechToText =
|
||||||
|
`[audio] ${await this.openaiService.speechToText(messageRaw, this)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4662,26 +4757,28 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareMessage(message: proto.IWebMessageInfo): any {
|
private prepareMessage(message: WAMessage): Message {
|
||||||
const contentType = getContentType(message.message);
|
const keyAny = message.key as any;
|
||||||
const contentMsg = message?.message[contentType] as any;
|
const messageRaw: any = {
|
||||||
|
key: {
|
||||||
const messageRaw = {
|
...message.key,
|
||||||
key: message.key, // Save key exactly as it comes from Baileys
|
remoteJid: keyAny.remoteJid?.replace(/:.*$/, ''),
|
||||||
|
participant: keyAny.participant?.replace(/:.*$/, ''),
|
||||||
|
},
|
||||||
pushName:
|
pushName:
|
||||||
message.pushName ||
|
message.pushName ||
|
||||||
(message.key.fromMe
|
(message.key.fromMe
|
||||||
? 'Você'
|
? 'Você'
|
||||||
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
: message?.participant || (message.key?.participant ? message.key.participant.split('@')[0] : null)),
|
||||||
status: status[message.status],
|
|
||||||
message: this.deserializeMessageBuffers({ ...message.message }),
|
message: this.deserializeMessageBuffers({ ...message.message }),
|
||||||
contextInfo: this.deserializeMessageBuffers(contentMsg?.contextInfo),
|
messageType: getContentType(message.message),
|
||||||
messageType: contentType || 'unknown',
|
|
||||||
messageTimestamp: Long.isLong(message.messageTimestamp)
|
messageTimestamp: Long.isLong(message.messageTimestamp)
|
||||||
? message.messageTimestamp.toNumber()
|
? message.messageTimestamp.toNumber()
|
||||||
: (message.messageTimestamp as number),
|
: (message.messageTimestamp as number),
|
||||||
|
source: getDevice(keyAny.id),
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
source: getDevice(message.key.id),
|
status: status[message.status],
|
||||||
|
contextInfo: this.deserializeMessageBuffers(message.message?.messageContextInfo),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!messageRaw.status && message.key.fromMe === false) {
|
if (!messageRaw.status && message.key.fromMe === false) {
|
||||||
@@ -5382,6 +5479,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
this.logger.error(`Error decrypting poll votes: ${error}`);
|
this.logger.error(`Error decrypting poll votes: ${error}`);
|
||||||
throw new InternalServerErrorException('Error decrypting poll votes', error.toString());
|
throw new InternalServerErrorException('Error decrypting poll votes', error.toString());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async fetchChannels(query: Query<Contact>) {
|
public async fetchChannels(query: Query<Contact>) {
|
||||||
const page = Number((query as any)?.page ?? 1);
|
const page = Number((query as any)?.page ?? 1);
|
||||||
|
|||||||
@@ -124,9 +124,20 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (instance?.enabled && regex.test(instance.url)) {
|
if (instance?.enabled && regex.test(instance.url)) {
|
||||||
|
// Add custom headers for better webhook tracking and debugging
|
||||||
|
const enhancedHeaders = {
|
||||||
|
...webhookHeaders,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Instance-ID': this.monitor.waInstances[instanceName].instanceId,
|
||||||
|
'X-Instance-Name': instanceName,
|
||||||
|
'X-Event-Type': event,
|
||||||
|
'X-Timestamp': Date.now().toString(),
|
||||||
|
'User-Agent': 'EvolutionAPI-Webhook/2.3.7',
|
||||||
|
};
|
||||||
|
|
||||||
const httpService = axios.create({
|
const httpService = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: webhookHeaders as Record<string, string> | undefined,
|
headers: enhancedHeaders as Record<string, string>,
|
||||||
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,10 @@ export class ChatRouter extends RouterBroker {
|
|||||||
schema: decryptPollVoteSchema,
|
schema: decryptPollVoteSchema,
|
||||||
ClassRef: DecryptPollVoteDto,
|
ClassRef: DecryptPollVoteDto,
|
||||||
execute: (instance, data) => chatController.decryptPollVote(instance, data),
|
execute: (instance, data) => chatController.decryptPollVote(instance, data),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(HttpStatus.OK).json(response);
|
||||||
|
})
|
||||||
.post(this.routerPath('findChannels'), ...guards, async (req, res) => {
|
.post(this.routerPath('findChannels'), ...guards, async (req, res) => {
|
||||||
const response = await this.dataValidate({
|
const response = await this.dataValidate({
|
||||||
request: req,
|
request: req,
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ export type Webhook = {
|
|||||||
};
|
};
|
||||||
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
|
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
|
||||||
export type ConfigSessionPhone = { CLIENT: string; NAME: string };
|
export type ConfigSessionPhone = { CLIENT: string; NAME: string };
|
||||||
|
export type Baileys = { VERSION?: string };
|
||||||
export type QrCode = { LIMIT: number; COLOR: string };
|
export type QrCode = { LIMIT: number; COLOR: string };
|
||||||
export type Typebot = { ENABLED: boolean; API_VERSION: string; SEND_MEDIA_BASE64: boolean };
|
export type Typebot = { ENABLED: boolean; API_VERSION: string; SEND_MEDIA_BASE64: boolean };
|
||||||
export type Chatwoot = {
|
export type Chatwoot = {
|
||||||
@@ -410,6 +411,7 @@ export interface Env {
|
|||||||
WEBHOOK: Webhook;
|
WEBHOOK: Webhook;
|
||||||
PUSHER: Pusher;
|
PUSHER: Pusher;
|
||||||
CONFIG_SESSION_PHONE: ConfigSessionPhone;
|
CONFIG_SESSION_PHONE: ConfigSessionPhone;
|
||||||
|
BAILEYS: Baileys;
|
||||||
QRCODE: QrCode;
|
QRCODE: QrCode;
|
||||||
TYPEBOT: Typebot;
|
TYPEBOT: Typebot;
|
||||||
CHATWOOT: Chatwoot;
|
CHATWOOT: Chatwoot;
|
||||||
@@ -800,6 +802,9 @@ export class ConfigService {
|
|||||||
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
|
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
|
||||||
NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome',
|
NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'Chrome',
|
||||||
},
|
},
|
||||||
|
BAILEYS: {
|
||||||
|
VERSION: process.env?.CONFIG_BAILEYS_VERSION,
|
||||||
|
},
|
||||||
QRCODE: {
|
QRCODE: {
|
||||||
LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30,
|
LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30,
|
||||||
COLOR: process.env.QRCODE_COLOR || '#198754',
|
COLOR: process.env.QRCODE_COLOR || '#198754',
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
import { fetchLatestBaileysVersion, WAVersion } from 'baileys';
|
import { fetchLatestBaileysVersion, WAVersion } from 'baileys';
|
||||||
|
|
||||||
|
import { Baileys, configService } from '../config/env.config';
|
||||||
|
|
||||||
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
|
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
|
||||||
|
// Check if manual version is set via configuration
|
||||||
|
const baileysConfig = configService.get<Baileys>('BAILEYS');
|
||||||
|
const manualVersion = baileysConfig?.VERSION;
|
||||||
|
|
||||||
|
if (manualVersion) {
|
||||||
|
const versionParts = manualVersion.split('.').map(Number);
|
||||||
|
if (versionParts.length === 3 && !versionParts.some(isNaN)) {
|
||||||
|
return {
|
||||||
|
version: versionParts as WAVersion,
|
||||||
|
isLatest: false,
|
||||||
|
isManual: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
|
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { prismaRepository } from '@api/server.module';
|
import { prismaRepository } from '@api/server.module';
|
||||||
import { configService, Database } from '@config/env.config';
|
import { configService, Database } from '@config/env.config';
|
||||||
import { Logger } from '@config/logger.config';
|
import { Logger } from '@config/logger.config';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const logger = new Logger('OnWhatsappCache');
|
const logger = new Logger('OnWhatsappCache');
|
||||||
@@ -164,9 +165,28 @@ export async function saveOnWhatsappCache(data: ISaveOnWhatsappCacheParams[]) {
|
|||||||
logger.verbose(
|
logger.verbose(
|
||||||
`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
|
`[saveOnWhatsappCache] Register does not exist, creating: remoteJid=${remoteJid}, jidOptions=${dataPayload.jidOptions}, lid=${dataPayload.lid}`,
|
||||||
);
|
);
|
||||||
await prismaRepository.isOnWhatsapp.create({
|
try {
|
||||||
data: dataPayload,
|
await prismaRepository.isOnWhatsapp.create({
|
||||||
});
|
data: dataPayload,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
// Check for unique constraint violation (Prisma error code P2002)
|
||||||
|
if (
|
||||||
|
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||||
|
error.code === 'P2002' &&
|
||||||
|
(error.meta?.target as string[])?.includes('remoteJid')
|
||||||
|
) {
|
||||||
|
logger.verbose(
|
||||||
|
`[saveOnWhatsappCache] Race condition detected for ${remoteJid}, updating existing record instead.`,
|
||||||
|
);
|
||||||
|
await prismaRepository.isOnWhatsapp.update({
|
||||||
|
where: { remoteJid: remoteJid },
|
||||||
|
data: dataPayload,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Loga o erro mas não para a execução dos outros promises
|
// Loga o erro mas não para a execução dos outros promises
|
||||||
|
|||||||
Reference in New Issue
Block a user