@areumtecnologia/baileys 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/baileys.code-workspace +8 -0
- package/desktop.ini +0 -0
- package/handlers/desktop.ini +0 -0
- package/handlers/groups.js +94 -0
- package/handlers/messages.js +285 -0
- package/handlers/presence-status.js +9 -0
- package/handlers/users.js +94 -0
- package/index.js +456 -0
- package/package.json +16 -0
- package/tests/app.js +68 -0
- package/tests/desktop.ini +0 -0
- package/types/buttons.js +72 -0
- package/types/desktop.ini +0 -0
- package/types/interactive-messages.js +318 -0
- package/utils/desktop.ini +0 -0
- package/utils/generic-utils.js +23 -0
- package/utils/index.js +4 -0
- package/utils/message-normalizer.js +323 -0
- package/utils/message-store-db-handler.js +39 -0
- package/utils/message-store.js +67 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// File: lib/builders/interactive-message.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Classe base abstrata para todos os botões interativos.
|
|
5
|
+
* @abstract
|
|
6
|
+
*/
|
|
7
|
+
class InteractiveButtonBase {
|
|
8
|
+
constructor(name) {
|
|
9
|
+
if (this.constructor === InteractiveButtonBase) {
|
|
10
|
+
throw new Error("A classe base abstrata não pode ser instanciada diretamente.");
|
|
11
|
+
}
|
|
12
|
+
this.name = name;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Formata o botão para o formato final esperado pela API.
|
|
17
|
+
* @returns {{name: string, buttonParamsJson: string}}
|
|
18
|
+
*/
|
|
19
|
+
toJSON() {
|
|
20
|
+
const params = this._buildParams();
|
|
21
|
+
return {
|
|
22
|
+
name: this.name,
|
|
23
|
+
buttonParamsJson: JSON.stringify(params)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Método a ser implementado pelas subclasses para construir seus parâmetros específicos.
|
|
29
|
+
* @protected
|
|
30
|
+
* @abstract
|
|
31
|
+
*/
|
|
32
|
+
_buildParams() {
|
|
33
|
+
throw new Error("O método '_buildParams' deve ser implementado pela subclasse.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// =================================================================================================
|
|
38
|
+
// CLASSES DE BOTÕES SIMPLES
|
|
39
|
+
// =================================================================================================
|
|
40
|
+
|
|
41
|
+
class QuickReplyButton extends InteractiveButtonBase {
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} id - O ID que será retornado quando o botão for clicado.
|
|
44
|
+
* @param {string} displayText - O texto exibido no botão.
|
|
45
|
+
*/
|
|
46
|
+
constructor(id, displayText) {
|
|
47
|
+
super('quick_reply');
|
|
48
|
+
this.id = id;
|
|
49
|
+
this.displayText = displayText;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_buildParams() {
|
|
53
|
+
return { display_text: this.displayText, id: this.id };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class UrlButton extends InteractiveButtonBase {
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} displayText - O texto exibido no botão.
|
|
60
|
+
* @param {string} url - A URL para a qual o usuário será redirecionado.
|
|
61
|
+
* @param {string} [merchantUrl] - URL do comerciante (opcional).
|
|
62
|
+
*/
|
|
63
|
+
constructor(displayText, url, merchantUrl = null) {
|
|
64
|
+
super('cta_url');
|
|
65
|
+
this.displayText = displayText;
|
|
66
|
+
this.url = url;
|
|
67
|
+
this.merchantUrl = merchantUrl;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_buildParams() {
|
|
71
|
+
const params = { display_text: this.displayText, url: this.url };
|
|
72
|
+
if (this.merchantUrl) {
|
|
73
|
+
params.merchant_url = this.merchantUrl;
|
|
74
|
+
}
|
|
75
|
+
return params;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class CopyCodeButton extends InteractiveButtonBase {
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} displayText - O texto exibido no botão.
|
|
82
|
+
* @param {string} code - O código a ser copiado para a área de transferência.
|
|
83
|
+
*/
|
|
84
|
+
constructor(displayText, code) {
|
|
85
|
+
super('cta_copy');
|
|
86
|
+
this.displayText = displayText;
|
|
87
|
+
this.code = code;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_buildParams() {
|
|
91
|
+
return { display_text: this.displayText, copy_code: this.code };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class CallButton extends InteractiveButtonBase {
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} displayText - O texto exibido no botão.
|
|
98
|
+
* @param {string} phoneNumber - O número de telefone a ser chamado.
|
|
99
|
+
*/
|
|
100
|
+
constructor(displayText, phoneNumber) {
|
|
101
|
+
super('cta_call');
|
|
102
|
+
this.displayText = displayText;
|
|
103
|
+
this.phoneNumber = phoneNumber;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_buildParams() {
|
|
107
|
+
return { display_text: this.displayText, phone_number: this.phoneNumber };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class LocationButton extends InteractiveButtonBase {
|
|
112
|
+
/**
|
|
113
|
+
* @param {string} displayText - O texto exibido no botão.
|
|
114
|
+
*/
|
|
115
|
+
constructor(displayText = 'Share Location') {
|
|
116
|
+
super('send_location');
|
|
117
|
+
this.displayText = displayText;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_buildParams() {
|
|
121
|
+
return { display_text: this.displayText };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =================================================================================================
|
|
126
|
+
// CLASSES PARA BOTÃO DE LISTA (SINGLE_SELECT)
|
|
127
|
+
// =================================================================================================
|
|
128
|
+
|
|
129
|
+
class ListRow {
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} id - O ID da linha, retornado na seleção.
|
|
132
|
+
* @param {string} title - O título principal da linha.
|
|
133
|
+
* @param {string} [description=''] - A descrição opcional abaixo do título.
|
|
134
|
+
* @param {string} [header=''] - O cabeçalho opcional da linha.
|
|
135
|
+
*/
|
|
136
|
+
constructor(id, title, description = '', header = '') {
|
|
137
|
+
this.id = id;
|
|
138
|
+
this.title = title;
|
|
139
|
+
this.description = description;
|
|
140
|
+
this.header = header;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
toJSON() {
|
|
144
|
+
return {
|
|
145
|
+
id: this.id,
|
|
146
|
+
title: this.title,
|
|
147
|
+
description: this.description,
|
|
148
|
+
header: this.header
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class ListSection {
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} title - O título da seção.
|
|
156
|
+
* @param {ListRow[]} [rows=[]] - Uma lista inicial de linhas.
|
|
157
|
+
*/
|
|
158
|
+
constructor(title, rows = []) {
|
|
159
|
+
this.title = title;
|
|
160
|
+
this.rows = rows;
|
|
161
|
+
this.highlightLabel = '';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Adiciona uma linha à seção.
|
|
166
|
+
* @param {ListRow} row - A linha a ser adicionada.
|
|
167
|
+
* @returns {ListSection}
|
|
168
|
+
*/
|
|
169
|
+
addRow(row) {
|
|
170
|
+
this.rows.push(row);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Adiciona uma etiqueta de destaque à seção.
|
|
176
|
+
* @param {string} label - O texto da etiqueta.
|
|
177
|
+
* @returns {ListSection}
|
|
178
|
+
*/
|
|
179
|
+
withHighlightLabel(label) {
|
|
180
|
+
this.highlightLabel = label;
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
toJSON() {
|
|
185
|
+
return {
|
|
186
|
+
title: this.title,
|
|
187
|
+
highlight_label: this.highlightLabel,
|
|
188
|
+
rows: this.rows.map(row => row.toJSON())
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
class ListButton extends InteractiveButtonBase {
|
|
194
|
+
/**
|
|
195
|
+
* @param {string} title - O título do menu da lista.
|
|
196
|
+
* @param {ListSection[]} [sections=[]] - Uma lista inicial de seções.
|
|
197
|
+
*/
|
|
198
|
+
constructor(title, sections = []) {
|
|
199
|
+
super('single_select');
|
|
200
|
+
this.title = title;
|
|
201
|
+
this.sections = sections;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Adiciona uma seção à lista.
|
|
206
|
+
* @param {ListSection} section - A seção a ser adicionada.
|
|
207
|
+
* @returns {ListButton}
|
|
208
|
+
*/
|
|
209
|
+
addSection(section) {
|
|
210
|
+
this.sections.push(section);
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_buildParams() {
|
|
215
|
+
return {
|
|
216
|
+
title: this.title,
|
|
217
|
+
sections: this.sections.map(sec => sec.toJSON())
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =================================================================================================
|
|
223
|
+
// CLASSE PRINCIPAL DO CONSTRUTOR
|
|
224
|
+
// =================================================================================================
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Construtor para mensagens interativas.
|
|
228
|
+
*/
|
|
229
|
+
class InteractiveMessage {
|
|
230
|
+
constructor() {
|
|
231
|
+
this.content = {
|
|
232
|
+
text: '',
|
|
233
|
+
title: '',
|
|
234
|
+
subtitle: '',
|
|
235
|
+
footer: '',
|
|
236
|
+
interactiveButtons: []
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Define o texto principal da mensagem.
|
|
242
|
+
* @param {string} text
|
|
243
|
+
* @returns {InteractiveMessage}
|
|
244
|
+
*/
|
|
245
|
+
withText(text) {
|
|
246
|
+
this.content.text = text;
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Define o título (cabeçalho) da mensagem.
|
|
252
|
+
* @param {string} title
|
|
253
|
+
* @returns {InteractiveMessage}
|
|
254
|
+
*/
|
|
255
|
+
withTitle(title) {
|
|
256
|
+
this.content.title = title;
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Define o subtítulo da mensagem.
|
|
262
|
+
* @param {string} subtitle
|
|
263
|
+
* @returns {InteractiveMessage}
|
|
264
|
+
*/
|
|
265
|
+
withSubtitle(subtitle) {
|
|
266
|
+
this.content.subtitle = subtitle;
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Define o rodapé da mensagem.
|
|
272
|
+
* @param {string} footer
|
|
273
|
+
* @returns {InteractiveMessage}
|
|
274
|
+
*/
|
|
275
|
+
withFooter(footer) {
|
|
276
|
+
this.content.footer = footer;
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Adiciona um botão à mensagem.
|
|
282
|
+
* @param {InteractiveButtonBase} button - Uma instância de uma classe de botão (ex: QuickReplyButton).
|
|
283
|
+
* @returns {InteractiveMessage}
|
|
284
|
+
*/
|
|
285
|
+
addButton(button) {
|
|
286
|
+
if (!(button instanceof InteractiveButtonBase)) {
|
|
287
|
+
throw new Error('O botão deve ser uma instância de uma classe que herda de InteractiveButtonBase.');
|
|
288
|
+
}
|
|
289
|
+
this.content.interactiveButtons.push(button.toJSON());
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Constrói e retorna o objeto final da mensagem interativa.
|
|
295
|
+
* @returns {object}
|
|
296
|
+
*/
|
|
297
|
+
build() {
|
|
298
|
+
// Remove chaves vazias para um payload mais limpo
|
|
299
|
+
Object.keys(this.content).forEach(key => {
|
|
300
|
+
if (!this.content[key] || (Array.isArray(this.content[key]) && this.content[key].length === 0)) {
|
|
301
|
+
delete this.content[key];
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return this.content;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
InteractiveMessage,
|
|
310
|
+
QuickReplyButton,
|
|
311
|
+
UrlButton,
|
|
312
|
+
CopyCodeButton,
|
|
313
|
+
CallButton,
|
|
314
|
+
LocationButton,
|
|
315
|
+
ListButton,
|
|
316
|
+
ListSection,
|
|
317
|
+
ListRow
|
|
318
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class Utils {
|
|
2
|
+
/**
|
|
3
|
+
* Pausa a execução por um determinado número de milissegundos.
|
|
4
|
+
* @param {number} ms - O tempo em milissegundos para aguardar.
|
|
5
|
+
* @returns {Promise<void>}
|
|
6
|
+
*/
|
|
7
|
+
static delay(ms) {
|
|
8
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Helper function to convert base64 to Uint8Array
|
|
12
|
+
static base64ToUint8Array(base64) {
|
|
13
|
+
const binaryString = atob(base64);
|
|
14
|
+
const len = binaryString.length;
|
|
15
|
+
const bytes = new Uint8Array(len);
|
|
16
|
+
for (let i = 0; i < len; i++) {
|
|
17
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
18
|
+
}
|
|
19
|
+
return bytes;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = Utils;
|
package/utils/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
const digestSync = require('crypto-digest-sync');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Classe utilitária estática para normalizar o objeto de mensagem do Baileys
|
|
5
|
+
* em uma estrutura rica, consistente e completa. (Versão 3 - Final)
|
|
6
|
+
*/
|
|
7
|
+
class MessageNormalizer {
|
|
8
|
+
/**
|
|
9
|
+
* Ponto de entrada principal. Normaliza uma mensagem bruta do Baileys.
|
|
10
|
+
* @param {import('baileys').proto.WebMessageInfo} rawMessage - O objeto de mensagem bruto.
|
|
11
|
+
* @param {import('../client').default} client - A instância do cliente.
|
|
12
|
+
* @returns {object|null} Um objeto de mensagem normalizado ou null se não for uma mensagem válida.
|
|
13
|
+
*/
|
|
14
|
+
static async normalize(rawMessage, client) {
|
|
15
|
+
// if (!rawMessage || (!rawMessage.message && !rawMessage.messageStubType)) {
|
|
16
|
+
if (!rawMessage || !rawMessage.message) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Caso especial 1: Mensagem editada. A normalização é feita na mensagem interna.
|
|
21
|
+
const editedMsgContent = rawMessage.message?.protocolMessage?.editedMessage;
|
|
22
|
+
if (editedMsgContent) {
|
|
23
|
+
const originalKey = rawMessage.message.protocolMessage.key;
|
|
24
|
+
const normalizedEdit = this.normalize({ key: originalKey, message: editedMsgContent, pushName: rawMessage.pushName }, client);
|
|
25
|
+
if (normalizedEdit) {
|
|
26
|
+
normalizedEdit.isEdited = true;
|
|
27
|
+
normalizedEdit.id = originalKey.id; // Garante que o ID é o da mensagem original
|
|
28
|
+
}
|
|
29
|
+
return normalizedEdit;
|
|
30
|
+
}
|
|
31
|
+
const originalType = Object.keys(rawMessage.message)[0];
|
|
32
|
+
const type = this._getFriendlyType(rawMessage.message);
|
|
33
|
+
const messageContent = rawMessage.message[originalType];
|
|
34
|
+
const contextInfo = messageContent?.contextInfo;
|
|
35
|
+
const chatId = rawMessage.key.remoteJid.includes('@lid') ? rawMessage.key.remoteJidAlt : rawMessage.key.remoteJid;
|
|
36
|
+
const isGroup = chatId.endsWith('@g.us');
|
|
37
|
+
const clientJid = client.jidNormalizedUser(client.sock.user.id);
|
|
38
|
+
|
|
39
|
+
const normalized = {
|
|
40
|
+
id: rawMessage.key.id,
|
|
41
|
+
from: isGroup ? rawMessage.key.participant : rawMessage.key.fromMe ? clientJid : chatId,
|
|
42
|
+
to: isGroup ? rawMessage.key.participant : rawMessage.key.fromMe ? chatId : clientJid,
|
|
43
|
+
chatId: chatId,
|
|
44
|
+
timestamp: new Date(Number(rawMessage.messageTimestamp) * 1000),
|
|
45
|
+
fromMe: rawMessage.key.fromMe,
|
|
46
|
+
isGroup: isGroup,
|
|
47
|
+
sender: {
|
|
48
|
+
id: isGroup ? rawMessage.key.participant : rawMessage.key.fromMe ? clientJid : chatId,
|
|
49
|
+
pushName: rawMessage.pushName || ''
|
|
50
|
+
},
|
|
51
|
+
type,
|
|
52
|
+
body: this._extractBody(rawMessage.message),
|
|
53
|
+
hasMedia: false,
|
|
54
|
+
media: null,
|
|
55
|
+
location: null,
|
|
56
|
+
contacts: this._extractContacts(rawMessage.message),
|
|
57
|
+
isReply: !!contextInfo?.quotedMessage,
|
|
58
|
+
quotedMessage: null,
|
|
59
|
+
isForwarded: !!contextInfo?.isForwarded,
|
|
60
|
+
forwardingScore: contextInfo?.forwardingScore || 0,
|
|
61
|
+
mentions: contextInfo?.mentionedJid || [],
|
|
62
|
+
isMentioningMe: (contextInfo?.mentionedJid || []).includes(clientJid),
|
|
63
|
+
isEdited: false,
|
|
64
|
+
interactiveReply: this._extractInteractiveReply(rawMessage.message),
|
|
65
|
+
reaction: this._extractReaction(rawMessage.message),
|
|
66
|
+
pollUpdate: await this._extractPollUpdate(rawMessage, client),
|
|
67
|
+
poll: this._extractPollCreation(rawMessage),
|
|
68
|
+
raw: rawMessage // Referência ao objeto original para acesso avançado
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Preenche os campos de mídia e localização com detalhes específicos
|
|
72
|
+
this._enrichWithTypedData(normalized, rawMessage, client);
|
|
73
|
+
|
|
74
|
+
// Se for uma resposta, normaliza a mensagem citada recursivamente
|
|
75
|
+
if (normalized.isReply) {
|
|
76
|
+
const quotedRaw = {
|
|
77
|
+
key: {
|
|
78
|
+
remoteJid: chatId,
|
|
79
|
+
id: contextInfo.stanzaId,
|
|
80
|
+
fromMe: client.jidNormalizedUser(contextInfo.participant) === clientJid,
|
|
81
|
+
participant: contextInfo.participant
|
|
82
|
+
},
|
|
83
|
+
message: contextInfo.quotedMessage
|
|
84
|
+
};
|
|
85
|
+
normalized.quotedMessage = this.normalize(quotedRaw, client);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @private Retorna um tipo amigável baseado no conteúdo da mensagem.
|
|
93
|
+
*/
|
|
94
|
+
static _getFriendlyType(message) {
|
|
95
|
+
if (!message) return 'unknown'
|
|
96
|
+
|
|
97
|
+
if (message.conversation || message.extendedTextMessage) return 'text'
|
|
98
|
+
if (message.imageMessage) return 'image'
|
|
99
|
+
if (message.videoMessage) return 'video'
|
|
100
|
+
if (message.audioMessage) return 'audio'
|
|
101
|
+
if (message.documentMessage) return 'document'
|
|
102
|
+
if (message.stickerMessage) return 'sticker'
|
|
103
|
+
if (message.locationMessage) return 'location'
|
|
104
|
+
if (message.contactMessage || message.contactsArrayMessage) return 'contact'
|
|
105
|
+
if (message.buttonsResponseMessage || message.listResponseMessage) return 'interactive_reply'
|
|
106
|
+
if (message.reactionMessage) return 'reaction'
|
|
107
|
+
if (message.pollCreationMessage || message.pollCreationMessageV3) return 'poll_creation'
|
|
108
|
+
if (message.pollUpdateMessage) return 'poll_update'
|
|
109
|
+
|
|
110
|
+
return 'unknown'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
/** @private Extrai o conteúdo textual principal de qualquer tipo de mensagem. */
|
|
115
|
+
static _extractBody(message) {
|
|
116
|
+
return message?.conversation ||
|
|
117
|
+
message?.extendedTextMessage?.text ||
|
|
118
|
+
message?.imageMessage?.caption ||
|
|
119
|
+
message?.videoMessage?.caption ||
|
|
120
|
+
message?.buttonsResponseMessage?.selectedDisplayText ||
|
|
121
|
+
message?.listResponseMessage?.title || '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @private Extrai e formata os dados de mídia. */
|
|
125
|
+
static _enrichWithTypedData(normalized, rawMessage, client) {
|
|
126
|
+
const type = Object.keys(rawMessage.message)[0];
|
|
127
|
+
const messageContent = rawMessage.message[type];
|
|
128
|
+
const mediaTypes = ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'];
|
|
129
|
+
if (mediaTypes.includes(type)) {
|
|
130
|
+
normalized.hasMedia = true;
|
|
131
|
+
normalized.media = {
|
|
132
|
+
mimetype: messageContent.mimetype,
|
|
133
|
+
fileName: messageContent.fileName || null,
|
|
134
|
+
duration: messageContent.seconds || null,
|
|
135
|
+
isPtt: messageContent.ptt || false,
|
|
136
|
+
isGif: messageContent.gifPlayback || false,
|
|
137
|
+
isViewOnce: messageContent.viewOnce || false,
|
|
138
|
+
download: () => client.messages.download(rawMessage)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (type === 'locationMessage') {
|
|
143
|
+
normalized.location = {
|
|
144
|
+
latitude: messageContent.degreesLatitude,
|
|
145
|
+
longitude: messageContent.degreesLongitude
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** @private Extrai os dados de uma resposta interativa (botão/lista). */
|
|
151
|
+
static _extractInteractiveReply(message) {
|
|
152
|
+
const buttonReply = message?.buttonsResponseMessage;
|
|
153
|
+
if (buttonReply) return { id: buttonReply.selectedButtonId, text: buttonReply.selectedDisplayText };
|
|
154
|
+
|
|
155
|
+
const listReply = message?.listResponseMessage;
|
|
156
|
+
if (listReply) return { id: listReply.singleSelectReply?.selectedRowId, text: listReply.title };
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @private Extrai os dados de uma reação a uma mensagem. */
|
|
162
|
+
static _extractReaction(message) {
|
|
163
|
+
const reaction = message?.reactionMessage;
|
|
164
|
+
if (!reaction) return null;
|
|
165
|
+
return {
|
|
166
|
+
emoji: reaction.text,
|
|
167
|
+
reactedMessageId: reaction.key.id,
|
|
168
|
+
senderTimestamp: new Date(Number(reaction.senderTimestampMs))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @private Extrai os dados de um voto em uma enquete (descriptografado). */
|
|
173
|
+
static async _extractPollUpdate(msg, client) {
|
|
174
|
+
const isGroup = msg.key.remoteJid.endsWith('@g.us');
|
|
175
|
+
const clientJid = client.jidNormalizedUser(client.sock.user.id);
|
|
176
|
+
|
|
177
|
+
const message = msg.message;
|
|
178
|
+
const pollUpdate = message?.pollUpdateMessage;
|
|
179
|
+
if (!pollUpdate || !pollUpdate.pollCreationMessageKey) return null;
|
|
180
|
+
let creationMsg = null;
|
|
181
|
+
let pollEncKeyBuffer = null;
|
|
182
|
+
let decryptPollVoteParams = null;
|
|
183
|
+
try {
|
|
184
|
+
// Recupera a mensagem de criação da enquete diretamente da store
|
|
185
|
+
creationMsg = await client.store.getMessage(
|
|
186
|
+
pollUpdate.pollCreationMessageKey.remoteJid,
|
|
187
|
+
pollUpdate.pollCreationMessageKey.id
|
|
188
|
+
);
|
|
189
|
+
creationMsg.raw.message.pollCreationMessage = creationMsg.raw.message.pollCreationMessage ? creationMsg.raw.message.pollCreationMessage :
|
|
190
|
+
creationMsg.raw.message.pollCreationMessageV3;
|
|
191
|
+
|
|
192
|
+
// Verifica se a mensagem de criação existe e tem a estrutura correta
|
|
193
|
+
if (!creationMsg) {
|
|
194
|
+
console.warn("Mensagem de criação da enquete não encontrada na store ou sem estrutura válida");
|
|
195
|
+
return {
|
|
196
|
+
pollCreationMessageId: pollUpdate.pollCreationMessageKey?.id,
|
|
197
|
+
voterTimestamp: new Date(Number(pollUpdate.senderTimestampMs)),
|
|
198
|
+
selectedOptions: [],
|
|
199
|
+
error: "Poll creation message not found"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!creationMsg || !creationMsg.raw.message.pollCreationMessage) {
|
|
204
|
+
console.warn("Mensagem de criação da enquete sem estrutura válida");
|
|
205
|
+
return {
|
|
206
|
+
pollCreationMessageId: pollUpdate.pollCreationMessageKey?.id,
|
|
207
|
+
voterTimestamp: new Date(Number(pollUpdate.senderTimestampMs)),
|
|
208
|
+
selectedOptions: [],
|
|
209
|
+
error: "Mensagem de criação da enquete sem estrutura válida"
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Verifica se temos os dados necessários para descriptografia
|
|
214
|
+
if (!creationMsg.raw.message.messageContextInfo.messageSecret ||
|
|
215
|
+
!creationMsg.raw.message.pollCreationMessage.name ||
|
|
216
|
+
!creationMsg.raw.message.pollCreationMessage.options) {
|
|
217
|
+
console.warn("Mensagem de criação da enquete não contém dados necessários para descriptografia");
|
|
218
|
+
return {
|
|
219
|
+
pollCreationMessageId: pollUpdate.pollCreationMessageKey?.id,
|
|
220
|
+
voterTimestamp: new Date(Number(pollUpdate.senderTimestampMs)),
|
|
221
|
+
selectedOptions: [],
|
|
222
|
+
error: "Incomplete poll creation data"
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pollEncKeyBuffer = Buffer.from(Object.values(creationMsg.raw.message.messageContextInfo.messageSecret));
|
|
227
|
+
decryptPollVoteParams = {
|
|
228
|
+
pollCreatorJid: creationMsg.raw.key.remoteJid,
|
|
229
|
+
pollMsgId: creationMsg.raw.key.id,
|
|
230
|
+
pollEncKey: pollEncKeyBuffer,
|
|
231
|
+
voterJid: isGroup ? msg.key.participant : msg.key.fromMe ? clientJid : msg.key.remoteJid,
|
|
232
|
+
};
|
|
233
|
+
// Descriptografa os votos usando a mensagem bruta da store
|
|
234
|
+
const decrypted = await client.decryptPollVote(pollUpdate.vote, decryptPollVoteParams);
|
|
235
|
+
|
|
236
|
+
const selectedOptions = [];
|
|
237
|
+
for (const decryptedHash of decrypted.selectedOptions) {
|
|
238
|
+
const hashHex = Buffer.from(decryptedHash).toString('hex').toUpperCase();
|
|
239
|
+
for (const option of creationMsg.raw.message.pollCreationMessage?.options || []) {
|
|
240
|
+
const hash = Buffer.from(digestSync("SHA-256", new TextEncoder().encode(Buffer.from(option.optionName).toString())))
|
|
241
|
+
.toString("hex")
|
|
242
|
+
.toUpperCase();
|
|
243
|
+
if (hashHex === hash) {
|
|
244
|
+
selectedOptions.push(option.optionName);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
pollCreationMessageId: pollUpdate.pollCreationMessageKey?.id,
|
|
252
|
+
voterTimestamp: new Date(Number(pollUpdate.senderTimestampMs)),
|
|
253
|
+
selectedOptions,
|
|
254
|
+
success: selectedOptions.length > 0
|
|
255
|
+
};
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error("Erro ao descriptografar voto:", err.message, {
|
|
258
|
+
msg,
|
|
259
|
+
pollUpdateMessage: pollUpdate,
|
|
260
|
+
decryptPollVoteParams,
|
|
261
|
+
errorStack: err.stack
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
pollCreationMessageId: pollUpdate.pollCreationMessageKey?.id,
|
|
266
|
+
voterTimestamp: new Date(Number(pollUpdate.senderTimestampMs)),
|
|
267
|
+
selectedOptions: [],
|
|
268
|
+
error: err.message,
|
|
269
|
+
code: err.code
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** @private Extrai os dados de uma mensagem de criação de enquete. */
|
|
275
|
+
static _extractPollCreation(raw) {
|
|
276
|
+
const message = raw.message;
|
|
277
|
+
const poll = message?.pollCreationMessage || message.pollCreationMessageV3;
|
|
278
|
+
if (!poll) return null
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
id: raw.key.id,
|
|
282
|
+
creator: {
|
|
283
|
+
jid: raw.key.remoteJid,
|
|
284
|
+
pushName: raw.pushName,
|
|
285
|
+
verifiedBizName: raw.verifiedBizName
|
|
286
|
+
},
|
|
287
|
+
name: poll.name,
|
|
288
|
+
selectableOptionsCount: poll.selectableOptionsCount,
|
|
289
|
+
allowsMultipleAnswers: poll.selectableOptionsCount > 1,
|
|
290
|
+
options: (poll.options || []).map(opt => ({
|
|
291
|
+
name: opt.optionName || null,
|
|
292
|
+
hash: opt.optionHash || null
|
|
293
|
+
})),
|
|
294
|
+
messageSecret: message.messageContextInfo?.messageSecret,
|
|
295
|
+
senderTimestamp: poll.messageTimestamp
|
|
296
|
+
? new Date(Number(poll.messageTimestamp) * 1000)
|
|
297
|
+
: null,
|
|
298
|
+
raw: poll
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @private Extrai os dados de contatos enviados. */
|
|
303
|
+
static _extractContacts(message) {
|
|
304
|
+
const parseVcard = (vcard = '') => {
|
|
305
|
+
const nameMatch = vcard.match(/FN:(.+)/);
|
|
306
|
+
const numberMatch = vcard.match(/waid=(\d+)/);
|
|
307
|
+
return {
|
|
308
|
+
name: (nameMatch ? nameMatch[1] : '').replace(/\\/g, ''),
|
|
309
|
+
number: numberMatch ? numberMatch[1] : ''
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const singleContact = message?.contactMessage;
|
|
314
|
+
if (singleContact) return [parseVcard(singleContact.vcard)];
|
|
315
|
+
|
|
316
|
+
const multiContact = message?.contactsArrayMessage?.contacts;
|
|
317
|
+
if (multiContact) return multiContact.map(c => parseVcard(c.vcard));
|
|
318
|
+
|
|
319
|
+
return [];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = MessageNormalizer;
|