@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.
@@ -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,4 @@
1
+ const MessageNormalizer = require('./message-normalizer');
2
+ const MessageStore = require('./message-store');
3
+ const Utils = require('./generic-utils');
4
+ module.exports = { MessageNormalizer, MessageStore, Utils };
@@ -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;