mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-18 11:22:21 -06:00
Add poll vote decryption endpoint and logic
Introduces a new API endpoint and supporting logic to decrypt WhatsApp poll votes. Adds DecryptPollVoteDto, validation schema, controller method, and service logic to process and aggregate poll vote results based on poll creation message key.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ArchiveChatDto,
|
||||
BlockUserDto,
|
||||
DecryptPollVoteDto,
|
||||
DeleteMessage,
|
||||
getBase64FromMediaMessageDto,
|
||||
MarkChatUnreadDto,
|
||||
@@ -113,4 +114,8 @@ export class ChatController {
|
||||
public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].blockUser(data);
|
||||
}
|
||||
|
||||
public async decryptPollVote({ instanceName }: InstanceDto, data: DecryptPollVoteDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(data.pollCreationMessageKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,3 +127,12 @@ export class BlockUserDto {
|
||||
number: string;
|
||||
status: 'block' | 'unblock';
|
||||
}
|
||||
|
||||
export class DecryptPollVoteDto {
|
||||
pollCreationMessageKey: {
|
||||
id: string;
|
||||
remoteJid: string;
|
||||
participant?: string;
|
||||
fromMe?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
ArchiveChatDto,
|
||||
BlockUserDto,
|
||||
DecryptPollVoteDto,
|
||||
DeleteMessage,
|
||||
getBase64FromMediaMessageDto,
|
||||
MarkChatUnreadDto,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
archiveChatSchema,
|
||||
blockUserSchema,
|
||||
contactValidateSchema,
|
||||
decryptPollVoteSchema,
|
||||
deleteMessageSchema,
|
||||
markChatUnreadSchema,
|
||||
messageUpSchema,
|
||||
@@ -281,6 +283,16 @@ export class ChatRouter extends RouterBroker {
|
||||
});
|
||||
|
||||
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,21 @@ export const buttonsMessageSchema: JSONSchema7 = {
|
||||
},
|
||||
required: ['number'],
|
||||
};
|
||||
|
||||
export const decryptPollVoteSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
pollCreationMessageKey: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
remoteJid: { type: 'string' },
|
||||
participant: { type: 'string' },
|
||||
fromMe: { type: 'boolean' },
|
||||
},
|
||||
required: ['id', 'remoteJid'],
|
||||
},
|
||||
},
|
||||
required: ['pollCreationMessageKey'],
|
||||
};
|
||||
Reference in New Issue
Block a user