Compare commits

..

7 Commits

Author SHA1 Message Date
Davidson Gomes
6ede76f8cc Merge branch 'develop' into develop 2025-12-12 17:51:51 -03:00
OrionDesign
2fee5053f3 fix(baileys): corrigir declaração de variável error em blocos catch 2025-12-11 17:11:58 -03:00
OrionDesign
67c4aa640b refactor(baileys): atualizar serviço de mensagens e schemas de validação 2025-12-11 17:01:13 -03:00
Davidson Gomes
604c9f968b Merge pull request #2296 from caiobleggi/main
Some checks failed
Build Docker image / Build and Deploy (push) Has been cancelled
Check Code Quality / check-lint-and-build (push) Has been cancelled
Security Scan / CodeQL Analysis (javascript) (push) Has been cancelled
Security Scan / Dependency Review (push) Has been cancelled
feat(channel): add support for @newsletter in sendMessage and findChannels
2025-12-11 10:08:54 -03:00
OrionDesign
076449e5d6 Refactor DecryptPollVoteDto and schema structure
Updated DecryptPollVoteDto to use a nested message.key structure and moved remoteJid to the top level. Adjusted the controller and validation schema to match the new structure for consistency and clarity.
2025-12-09 17:03:15 -03:00
OrionDesign
5faf3d18d6 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.
2025-12-09 16:56:46 -03:00
Caio Bleggi
52fa931140 feat(channel): add support for @newsletter in sendMessage and findChannels 2025-12-09 12:03:47 -03:00
7 changed files with 408 additions and 3 deletions

28
package-lock.json generated
View File

@@ -2907,6 +2907,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2928,6 +2929,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz",
"integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
@@ -2940,6 +2942,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2955,6 +2958,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.204.0.tgz",
"integrity": "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/api-logs": "0.204.0",
"import-in-the-middle": "^1.8.1",
@@ -3362,6 +3366,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -3378,6 +3383,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
@@ -3395,6 +3401,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz",
"integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=14"
}
@@ -3643,6 +3650,7 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@@ -4933,6 +4941,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -5108,6 +5117,7 @@
"integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.47.0",
"@typescript-eslint/types": "8.47.0",
@@ -5411,6 +5421,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5758,6 +5769,7 @@
"resolved": "https://registry.npmjs.org/audio-decode/-/audio-decode-2.2.3.tgz",
"integrity": "sha512-Z0lHvMayR/Pad9+O9ddzaBJE0DrhZkQlStrC1RwcAHF3AhQAsdwKHeLGK8fYKyp2DDU6xHxzGb4CLMui12yVrg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@wasm-audio-decoders/flac": "^0.2.4",
"@wasm-audio-decoders/ogg-vorbis": "^0.1.15",
@@ -6746,6 +6758,7 @@
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -7636,6 +7649,7 @@
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -7706,6 +7720,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -7762,6 +7777,7 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8368,6 +8384,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -10355,6 +10372,7 @@
"resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.0.tgz",
"integrity": "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jimp/core": "1.6.0",
"@jimp/diff": "1.6.0",
@@ -10585,6 +10603,7 @@
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.4.tgz",
"integrity": "sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
@@ -10680,6 +10699,7 @@
"resolved": "https://registry.npmjs.org/link-preview-js/-/link-preview-js-3.2.0.tgz",
"integrity": "sha512-FvrLltjOPGbTzt+RugbzM7g8XuUNLPO2U/INSLczrYdAA32E7nZVUrVL1gr61DGOArGJA2QkPGMEvNMLLsXREA==",
"license": "MIT",
"peer": true,
"dependencies": {
"cheerio": "1.0.0-rc.11",
"url": "0.11.0"
@@ -12600,6 +12620,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
@@ -12909,6 +12930,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12938,6 +12960,7 @@
"integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@prisma/config": "6.19.0",
"@prisma/engines": "6.19.0"
@@ -14029,6 +14052,7 @@
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
@@ -14871,6 +14895,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15059,6 +15084,7 @@
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -15707,6 +15733,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16222,6 +16249,7 @@
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"devOptional": true,
"license": "ISC",
"peer": true,
"bin": {
"yaml": "bin.mjs"
},

View File

@@ -1,6 +1,7 @@
import {
ArchiveChatDto,
BlockUserDto,
DecryptPollVoteDto,
DeleteMessage,
getBase64FromMediaMessageDto,
MarkChatUnreadDto,
@@ -113,4 +114,15 @@ 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) {
const pollCreationMessageKey = {
id: data.message.key.id,
remoteJid: data.remoteJid,
};
return await this.waMonitor.waInstances[instanceName].baileysDecryptPollVote(pollCreationMessageKey);
public async fetchChannels({ instanceName }: InstanceDto, query: Query<Contact>) {
return await this.waMonitor.waInstances[instanceName].fetchChannels(query);
}
}

View File

@@ -127,3 +127,12 @@ export class BlockUserDto {
number: string;
status: 'block' | 'unblock';
}
export class DecryptPollVoteDto {
message: {
key: {
id: string;
};
};
remoteJid: string;
}

View File

@@ -3511,9 +3511,24 @@ export class BaileysStartupService extends ChannelStartupService {
users: { number: string; jid: string; name?: string }[];
} = { groups: [], broadcast: [], users: [] };
const onWhatsapp: OnWhatsAppDto[] = [];
data.numbers.forEach((number) => {
const jid = createJid(number);
if (isJidNewsletter(jid)) {
onWhatsapp.push(
new OnWhatsAppDto(
jid,
true, // Newsletters are always valid
number,
undefined, // Can be fetched later if needed
'newsletter', // Indicate it's a newsletter type
),
);
return;
}
if (isJidGroup(jid)) {
jids.groups.push({ number, jid });
} else if (jid === 'status@broadcast') {
@@ -3523,8 +3538,6 @@ export class BaileysStartupService extends ChannelStartupService {
}
});
const onWhatsapp: OnWhatsAppDto[] = [];
// BROADCAST
onWhatsapp.push(...jids.broadcast.map(({ jid, number }) => new OnWhatsAppDto(jid, false, number)));
@@ -4700,6 +4713,10 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (isJidNewsletter(message.key.remoteJid) && message.key.fromMe) {
messageRaw.status = status[3]; // DELIVERED MESSAGE TO NEWSLETTER CHANNEL
}
return messageRaw;
}
@@ -5119,4 +5136,298 @@ 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 {
// 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());
}
public async fetchChannels(query: Query<Contact>) {
const page = Number((query as any)?.page ?? 1);
const limit = Number((query as any)?.limit ?? (query as any)?.rows ?? 50);
const skip = (page - 1) * limit;
const messages = await this.prismaRepository.message.findMany({
where: {
instanceId: this.instanceId,
AND: [{ key: { path: ['remoteJid'], not: null } }],
},
orderBy: { messageTimestamp: 'desc' },
select: {
key: true,
messageTimestamp: true,
},
});
const channelMap = new Map<string, { remoteJid: string; pushName: undefined; lastMessageTimestamp: number }>();
for (const msg of messages) {
const key = msg.key as any;
const remoteJid = key?.remoteJid as string | undefined;
if (!remoteJid || !isJidNewsletter(remoteJid)) continue;
if (!channelMap.has(remoteJid)) {
channelMap.set(remoteJid, {
remoteJid,
pushName: undefined, // Push name is never stored for channels, so we set it as undefined
lastMessageTimestamp: msg.messageTimestamp,
});
}
}
const allChannels = Array.from(channelMap.values());
const total = allChannels.length;
const pages = Math.ceil(total / limit);
const records = allChannels.slice(skip, skip + limit);
return {
total,
pages,
currentPage: page,
limit,
records,
};
}
}

View File

@@ -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,22 @@ 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),
.post(this.routerPath('findChannels'), ...guards, async (req, res) => {
const response = await this.dataValidate({
request: req,
schema: contactValidateSchema,
ClassRef: Query<Contact>,
execute: (instance, query) => chatController.fetchChannels(instance, query as any),
});
return res.status(HttpStatus.OK).json(response);
});
}

View File

@@ -35,7 +35,12 @@ function formatBRNumber(jid: string) {
export function createJid(number: string): string {
number = number.replace(/:\d+/, '');
if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) {
if (
number.includes('@g.us') ||
number.includes('@s.whatsapp.net') ||
number.includes('@lid') ||
number.includes('@newsletter')
) {
return number;
}

View File

@@ -447,3 +447,25 @@ export const buttonsMessageSchema: JSONSchema7 = {
},
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'],
};