mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2026-01-08 13:00:24 -06:00
Compare commits
2 Commits
2.3.7
...
076449e5d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076449e5d6 | ||
|
|
5faf3d18d6 |
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ArchiveChatDto,
|
ArchiveChatDto,
|
||||||
BlockUserDto,
|
BlockUserDto,
|
||||||
|
DecryptPollVoteDto,
|
||||||
DeleteMessage,
|
DeleteMessage,
|
||||||
getBase64FromMediaMessageDto,
|
getBase64FromMediaMessageDto,
|
||||||
MarkChatUnreadDto,
|
MarkChatUnreadDto,
|
||||||
@@ -113,4 +114,12 @@ export class ChatController {
|
|||||||
public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
|
public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
|
||||||
return await this.waMonitor.waInstances[instanceName].blockUser(data);
|
return await this.waMonitor.waInstances[instanceName].blockUser(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async decryptPollVote({ instanceName }: InstanceDto, data: DecryptPollVoteDto) {
|
||||||
|
const pollCreationMessageKey = {
|
||||||
|
id: data.message.key.id,
|
||||||
|
remoteJid: data.remoteJid,
|
||||||
|
};
|
||||||
|
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,3 +127,12 @@ export class BlockUserDto {
|
|||||||
number: string;
|
number: string;
|
||||||
status: 'block' | 'unblock';
|
status: 'block' | 'unblock';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DecryptPollVoteDto {
|
||||||
|
message: {
|
||||||
|
key: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
remoteJid: string;
|
||||||
|
}
|
||||||
@@ -5119,4 +5119,247 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async baileysDecryptPollVote(pollCreationMessageKey: proto.IMessageKey) {
|
||||||
|
try {
|
||||||
|
this.logger.verbose('Starting poll vote decryption process');
|
||||||
|
|
||||||
|
// Buscar a mensagem de criação da enquete
|
||||||
|
const pollCreationMessage = (await this.getMessage(pollCreationMessageKey, true)) as proto.IWebMessageInfo;
|
||||||
|
|
||||||
|
if (!pollCreationMessage) {
|
||||||
|
throw new NotFoundException('Poll creation message not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrair opções da enquete
|
||||||
|
const pollOptions =
|
||||||
|
(pollCreationMessage.message as any)?.pollCreationMessage?.options ||
|
||||||
|
(pollCreationMessage.message as any)?.pollCreationMessageV3?.options ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (!pollOptions || pollOptions.length === 0) {
|
||||||
|
throw new NotFoundException('Poll options not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recuperar chave de criptografia
|
||||||
|
const pollMessageSecret = (await this.getMessage(pollCreationMessageKey)) as any;
|
||||||
|
let pollEncKey = pollMessageSecret?.messageContextInfo?.messageSecret;
|
||||||
|
|
||||||
|
if (!pollEncKey) {
|
||||||
|
throw new NotFoundException('Poll encryption key not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizar chave de criptografia
|
||||||
|
if (typeof pollEncKey === 'string') {
|
||||||
|
pollEncKey = Buffer.from(pollEncKey, 'base64');
|
||||||
|
} else if (pollEncKey?.type === 'Buffer' && Array.isArray(pollEncKey.data)) {
|
||||||
|
pollEncKey = Buffer.from(pollEncKey.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(pollEncKey) && pollEncKey.length === 44) {
|
||||||
|
pollEncKey = Buffer.from(pollEncKey.toString('utf8'), 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar todas as mensagens de atualização de votos
|
||||||
|
const allPollUpdateMessages = await this.prismaRepository.message.findMany({
|
||||||
|
where: {
|
||||||
|
instanceId: this.instanceId,
|
||||||
|
messageType: 'pollUpdateMessage',
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
key: true,
|
||||||
|
message: true,
|
||||||
|
messageTimestamp: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.verbose(`Found ${allPollUpdateMessages.length} pollUpdateMessage messages in database`);
|
||||||
|
|
||||||
|
// Filtrar apenas mensagens relacionadas a esta enquete específica
|
||||||
|
const pollUpdateMessages = allPollUpdateMessages.filter((msg) => {
|
||||||
|
const pollUpdate = (msg.message as any)?.pollUpdateMessage;
|
||||||
|
if (!pollUpdate) return false;
|
||||||
|
|
||||||
|
const creationKey = pollUpdate.pollCreationMessageKey;
|
||||||
|
if (!creationKey) return false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
creationKey.id === pollCreationMessageKey.id &&
|
||||||
|
jidNormalizedUser(creationKey.remoteJid || '') === jidNormalizedUser(pollCreationMessageKey.remoteJid || '')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.verbose(`Filtered to ${pollUpdateMessages.length} matching poll update messages`);
|
||||||
|
|
||||||
|
// Preparar candidatos de JID para descriptografia
|
||||||
|
const creatorCandidates = [
|
||||||
|
this.instance.wuid,
|
||||||
|
this.client.user?.lid,
|
||||||
|
pollCreationMessage.key.participant,
|
||||||
|
(pollCreationMessage.key as any).participantAlt,
|
||||||
|
pollCreationMessage.key.remoteJid,
|
||||||
|
(pollCreationMessage.key as any).remoteJidAlt,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const uniqueCreators = [...new Set(creatorCandidates.map((id) => jidNormalizedUser(id)))];
|
||||||
|
|
||||||
|
// Processar votos
|
||||||
|
const votesByUser = new Map<string, { timestamp: number; selectedOptions: string[]; voterJid: string }>();
|
||||||
|
|
||||||
|
this.logger.verbose(`Processing ${pollUpdateMessages.length} poll update messages for decryption`);
|
||||||
|
|
||||||
|
for (const pollUpdateMsg of pollUpdateMessages) {
|
||||||
|
const pollVote = (pollUpdateMsg.message as any)?.pollUpdateMessage?.vote;
|
||||||
|
if (!pollVote) continue;
|
||||||
|
|
||||||
|
const key = pollUpdateMsg.key as any;
|
||||||
|
const voterCandidates = [
|
||||||
|
this.instance.wuid,
|
||||||
|
this.client.user?.lid,
|
||||||
|
key.participant,
|
||||||
|
key.participantAlt,
|
||||||
|
key.remoteJidAlt,
|
||||||
|
key.remoteJid,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const uniqueVoters = [...new Set(voterCandidates.map((id) => jidNormalizedUser(id)))];
|
||||||
|
|
||||||
|
let selectedOptionNames: string[] = [];
|
||||||
|
let successfulVoterJid: string | undefined;
|
||||||
|
|
||||||
|
// Verificar se o voto já está descriptografado
|
||||||
|
if (pollVote.selectedOptions && Array.isArray(pollVote.selectedOptions)) {
|
||||||
|
const selectedOptions = pollVote.selectedOptions;
|
||||||
|
this.logger.verbose('Vote already has selectedOptions, checking format');
|
||||||
|
|
||||||
|
// Verificar se são strings (já descriptografado) ou buffers (precisa descriptografar)
|
||||||
|
if (selectedOptions.length > 0 && typeof selectedOptions[0] === 'string') {
|
||||||
|
// Já está descriptografado como nomes de opções
|
||||||
|
selectedOptionNames = selectedOptions;
|
||||||
|
successfulVoterJid = uniqueVoters[0];
|
||||||
|
this.logger.verbose(`Using already decrypted vote: voter=${successfulVoterJid}, options=${selectedOptionNames.join(',')}`);
|
||||||
|
} else {
|
||||||
|
// Está como hash, precisa converter para nomes
|
||||||
|
selectedOptionNames = pollOptions
|
||||||
|
.filter((option: any) => {
|
||||||
|
const hash = createHash('sha256').update(option.optionName).digest();
|
||||||
|
return selectedOptions.some((selected: any) => {
|
||||||
|
if (Buffer.isBuffer(selected)) {
|
||||||
|
return Buffer.compare(selected, hash) === 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.map((option: any) => option.optionName);
|
||||||
|
successfulVoterJid = uniqueVoters[0];
|
||||||
|
}
|
||||||
|
} else if (pollVote.encPayload && pollEncKey) {
|
||||||
|
// Tentar descriptografar
|
||||||
|
let decryptedVote: any = null;
|
||||||
|
|
||||||
|
for (const creator of uniqueCreators) {
|
||||||
|
for (const voter of uniqueVoters) {
|
||||||
|
try {
|
||||||
|
decryptedVote = decryptPollVote(pollVote, {
|
||||||
|
pollCreatorJid: creator,
|
||||||
|
pollMsgId: pollCreationMessage.key.id,
|
||||||
|
pollEncKey,
|
||||||
|
voterJid: voter,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
if (decryptedVote) {
|
||||||
|
successfulVoterJid = voter;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Continue tentando outras combinações
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decryptedVote) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decryptedVote && decryptedVote.selectedOptions) {
|
||||||
|
// Converter hashes para nomes de opções
|
||||||
|
selectedOptionNames = pollOptions
|
||||||
|
.filter((option: any) => {
|
||||||
|
const hash = createHash('sha256').update(option.optionName).digest();
|
||||||
|
return decryptedVote.selectedOptions.some((selected: any) => {
|
||||||
|
if (Buffer.isBuffer(selected)) {
|
||||||
|
return Buffer.compare(selected, hash) === 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.map((option: any) => option.optionName);
|
||||||
|
|
||||||
|
this.logger.verbose(`Successfully decrypted vote for voter: ${successfulVoterJid}, creator: ${uniqueCreators[0]}`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Failed to decrypt vote. Last error: Could not decrypt with any combination`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.warn('Vote has no encPayload and no selectedOptions, skipping');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedOptionNames.length > 0 && successfulVoterJid) {
|
||||||
|
const normalizedVoterJid = jidNormalizedUser(successfulVoterJid);
|
||||||
|
const existingVote = votesByUser.get(normalizedVoterJid);
|
||||||
|
|
||||||
|
// Manter apenas o voto mais recente de cada usuário
|
||||||
|
if (!existingVote || pollUpdateMsg.messageTimestamp > existingVote.timestamp) {
|
||||||
|
votesByUser.set(normalizedVoterJid, {
|
||||||
|
timestamp: pollUpdateMsg.messageTimestamp,
|
||||||
|
selectedOptions: selectedOptionNames,
|
||||||
|
voterJid: successfulVoterJid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agrupar votos por opção
|
||||||
|
const results: Record<string, { votes: number; voters: string[] }> = {};
|
||||||
|
|
||||||
|
// Inicializar todas as opções com zero votos
|
||||||
|
pollOptions.forEach((option: any) => {
|
||||||
|
results[option.optionName] = {
|
||||||
|
votes: 0,
|
||||||
|
voters: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agregar votos
|
||||||
|
votesByUser.forEach((voteData) => {
|
||||||
|
voteData.selectedOptions.forEach((optionName) => {
|
||||||
|
if (results[optionName]) {
|
||||||
|
results[optionName].votes++;
|
||||||
|
if (!results[optionName].voters.includes(voteData.voterJid)) {
|
||||||
|
results[optionName].voters.push(voteData.voterJid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obter nome da enquete
|
||||||
|
const pollName =
|
||||||
|
(pollCreationMessage.message as any)?.pollCreationMessage?.name ||
|
||||||
|
(pollCreationMessage.message as any)?.pollCreationMessageV3?.name ||
|
||||||
|
'Enquete sem nome';
|
||||||
|
|
||||||
|
// Calcular total de votos únicos
|
||||||
|
const totalVotes = votesByUser.size;
|
||||||
|
|
||||||
|
return {
|
||||||
|
poll: {
|
||||||
|
name: pollName,
|
||||||
|
totalVotes,
|
||||||
|
results,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error decrypting poll votes: ${error}`);
|
||||||
|
throw new InternalServerErrorException('Error decrypting poll votes', error.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { RouterBroker } from '@api/abstract/abstract.router';
|
|||||||
import {
|
import {
|
||||||
ArchiveChatDto,
|
ArchiveChatDto,
|
||||||
BlockUserDto,
|
BlockUserDto,
|
||||||
|
DecryptPollVoteDto,
|
||||||
DeleteMessage,
|
DeleteMessage,
|
||||||
getBase64FromMediaMessageDto,
|
getBase64FromMediaMessageDto,
|
||||||
MarkChatUnreadDto,
|
MarkChatUnreadDto,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
archiveChatSchema,
|
archiveChatSchema,
|
||||||
blockUserSchema,
|
blockUserSchema,
|
||||||
contactValidateSchema,
|
contactValidateSchema,
|
||||||
|
decryptPollVoteSchema,
|
||||||
deleteMessageSchema,
|
deleteMessageSchema,
|
||||||
markChatUnreadSchema,
|
markChatUnreadSchema,
|
||||||
messageUpSchema,
|
messageUpSchema,
|
||||||
@@ -281,6 +283,16 @@ export class ChatRouter extends RouterBroker {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.status(HttpStatus.CREATED).json(response);
|
return res.status(HttpStatus.CREATED).json(response);
|
||||||
|
})
|
||||||
|
.post(this.routerPath('getPollVote'), ...guards, async (req, res) => {
|
||||||
|
const response = await this.dataValidate<DecryptPollVoteDto>({
|
||||||
|
request: req,
|
||||||
|
schema: decryptPollVoteSchema,
|
||||||
|
ClassRef: DecryptPollVoteDto,
|
||||||
|
execute: (instance, data) => chatController.decryptPollVote(instance, data),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(HttpStatus.OK).json(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -447,3 +447,25 @@ export const buttonsMessageSchema: JSONSchema7 = {
|
|||||||
},
|
},
|
||||||
required: ['number'],
|
required: ['number'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const decryptPollVoteSchema: JSONSchema7 = {
|
||||||
|
$id: v4(),
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
message: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['id'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['key'],
|
||||||
|
},
|
||||||
|
remoteJid: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['message', 'remoteJid'],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user