@areumtecnologia/wasockets 2.3.0 → 2.3.1

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/README.md CHANGED
@@ -224,18 +224,33 @@ client.on(Events.LABELS_UPDATE, (labels) => { /* Criação ou associação de et
224
224
  Ao escutar `Events.MESSAGE_RECEIVED`, você recebe um objeto `ctx` que já contém todos os dados mastigados e métodos de ação vinculados diretamente àquela mensagem:
225
225
 
226
226
  #### Propriedades de `ctx`:
227
- - `ctx.id`: ID único da mensagem.
227
+ - `ctx.id`: ID único da mensagem (se for mensagem editada, preserva o ID original rastreável).
228
228
  - `ctx.chatId`: JID do chat (usuário, grupo ou canal).
229
- - `ctx.sender`: JID de quem enviou.
230
- - `ctx.senderNumber`: Apenas os números de telefone do remetente (sem `@s.whatsapp.net`).
231
- - `ctx.senderName`: Nome formatado (nome salvo, apelido comercial ou pushName).
229
+ - `ctx.fromMe` / `ctx.isFromMe`: Booleano se a mensagem foi enviada pelo próprio bot.
230
+ - `ctx.from`: JID de origem (remetente em recebidas, ou JID do bot em enviadas).
231
+ - `ctx.to`: JID de destino (bot em recebidas, ou JID do chat/destinatário em enviadas).
232
+ - `ctx.fromPushName` / `ctx.senderName`: Nome de exibição formatado ou pushName ('Você' se `fromMe`).
233
+ - `ctx.fromNumber` / `ctx.senderNumber`: Apenas os números de telefone do remetente (sem sufixos ou `@s.whatsapp.net`).
234
+ - `ctx.sender`: JID de quem enviou a mensagem no chat.
232
235
  - `ctx.isGroup`: Booleano indicando se é grupo (`@g.us`).
233
236
  - `ctx.isChannel`: Booleano indicando se é newsletter/canal (`@newsletter`).
234
- - `ctx.isFromMe`: Booleano se a mensagem foi enviada pelo próprio bot.
235
- - `ctx.body`: Texto principal da mensagem (já decodificado de texto, legenda, botão ou lista).
236
- - `ctx.type`: Tipo normalizado (`text`, `image`, `video`, `audio`, `document`, `sticker`, `poll`, etc.).
237
- - `ctx.media`: Instância de `MediaObject` (se houver mídia na mensagem).
238
- - `ctx.quoted`: Instância recursiva de `MessageContext` para a mensagem que foi citada/respondida.
237
+ - `ctx.isBroadcast`: Booleano se é lista de transmissão ou status.
238
+ - `ctx.body`: Texto principal da mensagem (já desempacotado recursivamente de `viewOnce`, `ephemeral`, botões, listas, etc.).
239
+ - `ctx.type`: Tipo normalizado e amigável (28 tipos identificados: `text`, `image`, `video`, `voice`, `document`, `sticker`, `native_flow_response`, `poll_creation`, `event`, etc.).
240
+ - `ctx.isEdited`: Booleano indicando se a mensagem é uma edição de mensagem prévia (`protocolMessage.editedMessage`).
241
+ - `ctx.editTimestamp`: Instância de `Date` informando quando a mensagem foi editada (ou `null`).
242
+ - `ctx.interactiveReply`: Resposta estruturada de botões, listas e fluxos nativos (`{ id, text, params, flowName, type }`).
243
+ - `ctx.contacts`: Array com contatos e vCards parseados (`[{ name, number, org, vcard }]`), com suporte a `waid` e fallback `TEL`.
244
+ - `ctx.location`: Coordenadas e endereço (`{ latitude, longitude, name, address, url, isLive }`).
245
+ - `ctx.reaction`: Detalhes de reação (`{ emoji, reactedMessageId, targetKey, isRemoval, senderTimestamp }`).
246
+ - `ctx.poll`: Dados completos de enquetes (`{ name, options, selectableOptionsCount, allowsMultipleAnswers }`).
247
+ - `ctx.mediaDetails`: Metadados da mídia (`{ mimetype, fileName, caption, duration, isPtt, isGif, isViewOnce }`).
248
+ - `ctx.media`: Instância de `MediaObject` para conversão rápida para buffer, base64, salvamento em disco ou Sharp.
249
+ - `ctx.isMentioningMe`: Booleano indicando se o bot foi mencionado diretamente no texto (suporta Phone JID e LID JID).
250
+ - `ctx.mentions` / `ctx.mentionedJids`: Array de JIDs mencionados na mensagem.
251
+ - `ctx.isForwarded` & `ctx.forwardingScore`: Rastreamento de encaminhamento.
252
+ - `ctx.isReply`: Booleano indicando se esta mensagem está citando outra.
253
+ - `ctx.quoted`: Instância recursiva de `MessageContext` da mensagem citada/respondida (com controle de profundidade).
239
254
  - `ctx.raw`: O nó bruto original do WhatsApp/Baileys para casos de baixo nível.
240
255
 
241
256
  #### Ações Disponíveis em `ctx`:
@@ -10,58 +10,125 @@ class MessageContext {
10
10
  /**
11
11
  * @param {import('../WaClient').WaClient} client
12
12
  * @param {Object} rawMessage - proto.IWebMessageInfo
13
+ * @param {number} [depth=0] - Recursion depth guard for quoted messages
13
14
  */
14
- constructor(client, rawMessage) {
15
+ constructor(client, rawMessage, depth = 0) {
15
16
  this.client = client
16
- this.raw = rawMessage
17
+ this.raw = rawMessage || {}
18
+
19
+ // 1. Detection of Edited Message (protocolMessage.editedMessage)
20
+ const editedMsg = rawMessage?.message?.protocolMessage?.editedMessage
21
+ this.isEdited = Boolean(editedMsg)
22
+
23
+ const effectiveKey = (this.isEdited && rawMessage.message.protocolMessage.key)
24
+ ? { ...rawMessage.key, ...rawMessage.message.protocolMessage.key }
25
+ : (rawMessage?.key || {})
26
+
27
+ const effectiveMessage = editedMsg || rawMessage?.message || null
28
+
29
+ // 2. Identity and Routing
30
+ this.id = effectiveKey.id || rawMessage?.key?.id || ''
31
+ this.chatId = effectiveKey.remoteJid || rawMessage?.key?.remoteJid || ''
32
+ this.fromMe = Boolean(effectiveKey.fromMe)
33
+ this.isFromMe = this.fromMe
17
34
 
18
- const key = rawMessage.key || {}
19
- this.id = key.id || ''
20
- this.chatId = key.remoteJid || ''
21
- this.isFromMe = Boolean(key.fromMe)
22
35
  this.isGroup = JidNormalizer.isGroup(this.chatId)
23
36
  this.isChannel = JidNormalizer.isNewsletter(this.chatId)
24
37
  this.isBroadcast = JidNormalizer.isBroadcast(this.chatId)
25
38
 
26
- // Sender determination (group participant vs 1:1 chat remoteJid)
27
- this.sender = this.isGroup
28
- ? (key.participant || rawMessage.participant || this.chatId)
29
- : this.chatId
30
- this.senderNumber = JidNormalizer.toPhoneNumber(this.sender)
31
- this.senderName = rawMessage.pushName || this.senderNumber || 'Desconhecido'
32
-
33
- // Content Extraction
34
- const messageContent = rawMessage.message || null
35
- this.type = MessageNormalizer.extractType(messageContent)
36
- this.body = MessageNormalizer.extractBody(messageContent)
37
- this.mediaNode = MessageNormalizer.extractMediaNode(messageContent)
39
+ // Bot JID resolution
40
+ const sockUser = client?.sock?.user || {}
41
+ const clientJid = sockUser.id ? JidNormalizer.toJid(sockUser.id) : ''
42
+ const clientLid = sockUser.lid ? JidNormalizer.toJid(sockUser.lid) : ''
43
+
44
+ // Origin and Destination (from / to)
45
+ if (this.fromMe) {
46
+ this.from = clientJid || this.chatId
47
+ this.to = this.chatId
48
+ this.fromPushName = sockUser.name || sockUser.notify || 'Você'
49
+ } else {
50
+ this.from = this.isGroup
51
+ ? (effectiveKey.participant || rawMessage?.participant || this.chatId)
52
+ : this.chatId
53
+ this.to = clientJid || this.chatId
54
+ this.fromPushName = rawMessage?.pushName || JidNormalizer.toPhoneNumber(this.from) || 'Desconhecido'
55
+ }
56
+
57
+ this.sender = this.from
58
+ this.fromNumber = JidNormalizer.toPhoneNumber(this.from)
59
+ this.senderNumber = this.fromNumber
60
+ this.senderName = this.fromPushName
61
+
62
+ // Timestamps
63
+ const rawTs = Number(rawMessage?.messageTimestamp || Math.floor(Date.now() / 1000))
64
+ this.timestamp = new Date(rawTs > 10000000000 ? rawTs : rawTs * 1000)
65
+ this.editTimestamp = this.isEdited
66
+ ? new Date(Number(rawMessage.message.protocolMessage.timestampMs || Date.now()))
67
+ : null
68
+
69
+ // 3. Content Decoupling & Typed Parsers
70
+ this.type = MessageNormalizer.extractType(effectiveMessage)
71
+ this.body = MessageNormalizer.extractBody(effectiveMessage)
72
+ this.interactiveReply = MessageNormalizer.extractInteractiveReply(effectiveMessage)
73
+ this.contacts = MessageNormalizer.extractContacts(effectiveMessage)
74
+ this.location = MessageNormalizer.extractLocation(effectiveMessage)
75
+ this.reaction = MessageNormalizer.extractReaction(effectiveMessage)
76
+ this.poll = MessageNormalizer.extractPollCreation(effectiveMessage, rawMessage)
77
+
78
+ // 4. Media Normalization
79
+ this.mediaNode = MessageNormalizer.extractMediaNode(effectiveMessage)
38
80
  this.hasMedia = Boolean(this.mediaNode)
39
81
  this.media = this.hasMedia ? new MediaObject(this.mediaNode, this.type) : null
40
- this.timestamp = Number(rawMessage.messageTimestamp || Math.floor(Date.now() / 1000))
41
-
42
- // Context info & mentions
43
- this.contextInfo = MessageNormalizer.extractContextInfo(messageContent)
44
- this.mentionedJids = this.contextInfo?.mentionedJid || []
82
+ this.mediaDetails = MessageNormalizer.extractMediaDetails(effectiveMessage)
83
+
84
+ // 5. Context Info, Mentions & Forwarding
85
+ this.contextInfo = MessageNormalizer.extractContextInfo(effectiveMessage)
86
+ this.mentions = this.contextInfo?.mentionedJid || []
87
+ this.mentionedJids = this.mentions
88
+ this.isForwarded = Boolean(this.contextInfo?.isForwarded)
89
+ this.forwardingScore = Number(this.contextInfo?.forwardingScore || 0)
90
+
91
+ // Checks if the bot was directly mentioned (by PN or LID)
92
+ this.isMentioningMe = Boolean(
93
+ (clientJid && this.mentions.includes(clientJid)) ||
94
+ (clientLid && this.mentions.includes(clientLid))
95
+ )
45
96
 
46
- // Quoted message parsing
97
+ // 6. Quoted Message Parsing (Recursion with depth limit 1)
98
+ this.isReply = Boolean(this.contextInfo?.quotedMessage)
47
99
  this.quoted = null
48
- if (this.contextInfo?.quotedMessage) {
49
- const quotedMsg = this.contextInfo.quotedMessage
50
- this.quoted = {
51
- id: this.contextInfo.stanzaId,
52
- sender: this.contextInfo.participant,
53
- senderNumber: JidNormalizer.toPhoneNumber(this.contextInfo.participant),
54
- type: MessageNormalizer.extractType(quotedMsg),
55
- body: MessageNormalizer.extractBody(quotedMsg),
56
- raw: quotedMsg
100
+
101
+ if (this.isReply && this.contextInfo?.quotedMessage && depth === 0) {
102
+ const quotedParticipant = this.contextInfo.participant || this.chatId
103
+ const quotedRaw = {
104
+ key: {
105
+ remoteJid: this.chatId,
106
+ id: this.contextInfo.stanzaId,
107
+ fromMe: clientJid && JidNormalizer.toJid(quotedParticipant) === clientJid,
108
+ participant: quotedParticipant
109
+ },
110
+ message: this.contextInfo.quotedMessage,
111
+ pushName: this.contextInfo.pushName
112
+ }
113
+ try {
114
+ this.quoted = new MessageContext(this.client, quotedRaw, depth + 1)
115
+ } catch {
116
+ this.quoted = {
117
+ id: this.contextInfo.stanzaId,
118
+ sender: quotedParticipant,
119
+ senderNumber: JidNormalizer.toPhoneNumber(quotedParticipant),
120
+ type: MessageNormalizer.extractType(this.contextInfo.quotedMessage),
121
+ body: MessageNormalizer.extractBody(this.contextInfo.quotedMessage),
122
+ raw: this.contextInfo.quotedMessage
123
+ }
57
124
  }
58
125
  }
59
126
  }
60
127
 
61
128
  /**
62
129
  * Responde citando esta mensagem automaticamente
63
- * @param {string|Object} content - Texto ou objeto de mensagem do WaSockets
64
- * @param {Object} [options={}] - Opções adicionais de envio
130
+ * @param {string|Object} content - Texto ou payload de mensagem
131
+ * @param {Object} [options={}]
65
132
  */
66
133
  async reply(content, options = {}) {
67
134
  const payload = typeof content === 'string' ? { text: content } : content
@@ -72,8 +139,8 @@ class MessageContext {
72
139
  }
73
140
 
74
141
  /**
75
- * Reage à mensagem com um emoji
76
- * @param {string} emoji - Emoji único (ex: '👍', '❤️', '🔥')
142
+ * Reage à mensagem com um emoji (ou string vazia para remover)
143
+ * @param {string} emoji
77
144
  */
78
145
  async react(emoji) {
79
146
  return this.client.sendMessage(this.chatId, {
@@ -104,13 +171,13 @@ class MessageContext {
104
171
  * Marca esta mensagem como lida (ticks azuis)
105
172
  */
106
173
  async markRead() {
107
- if (!this.client.sock?.readMessages) return
174
+ if (!this.client?.sock?.readMessages) return
108
175
  return this.client.sock.readMessages([this.raw.key])
109
176
  }
110
177
 
111
178
  /**
112
179
  * Encaminha esta mensagem para outro destinatário
113
- * @param {string} targetJid - Destinatário
180
+ * @param {string} targetJid
114
181
  * @param {Object} [options={}]
115
182
  */
116
183
  async forward(targetJid, options = {}) {
@@ -122,10 +189,10 @@ class MessageContext {
122
189
 
123
190
  /**
124
191
  * Fixa a mensagem no chat com duração configurável
125
- * @param {number} [durationInSeconds=86400] - 86400 (24h), 604800 (7d), 2592000 (30d)
192
+ * @param {number} [durationInSeconds=86400]
126
193
  */
127
194
  async pin(durationInSeconds = 86400) {
128
- if (!this.client.sock?.pinInChatMessage) {
195
+ if (!this.client?.sock?.pinInChatMessage) {
129
196
  throw new Error('Método pinInChatMessage indisponível no socket.')
130
197
  }
131
198
  return this.client.sock.pinInChatMessage(this.chatId, this.raw.key, 1, durationInSeconds)
@@ -135,7 +202,7 @@ class MessageContext {
135
202
  * Desafixa a mensagem do topo da conversa
136
203
  */
137
204
  async unpin() {
138
- if (!this.client.sock?.pinInChatMessage) {
205
+ if (!this.client?.sock?.pinInChatMessage) {
139
206
  throw new Error('Método pinInChatMessage indisponível no socket.')
140
207
  }
141
208
  return this.client.sock.pinInChatMessage(this.chatId, this.raw.key, 2)
@@ -20,9 +20,17 @@ class EventNormalizer {
20
20
  if (!Array.isArray(messages)) return
21
21
 
22
22
  for (const rawMsg of messages) {
23
- // Ignore stub or protocol messages if empty
23
+ // Ignore stub or protocol messages if completely empty
24
24
  if (!rawMsg.message && !rawMsg.messageStubType) continue
25
25
 
26
+ // Handle protocol revoke messages explicitly
27
+ const isRevoke = rawMsg.message?.protocolMessage?.type === 0
28
+ if (isRevoke) {
29
+ client.emit(Events.MESSAGE_DELETE, {
30
+ keys: [rawMsg.message.protocolMessage.key || rawMsg.key]
31
+ })
32
+ }
33
+
26
34
  const ctx = new MessageContext(client, rawMsg)
27
35
 
28
36
  if (ctx.isFromMe) {
@@ -30,6 +38,15 @@ class EventNormalizer {
30
38
  } else {
31
39
  client.emit(Events.MESSAGE_RECEIVED, ctx, rawMsg)
32
40
  }
41
+
42
+ // If message is an edit, also notify on MESSAGE_UPDATE
43
+ if (ctx.isEdited) {
44
+ client.emit(Events.MESSAGE_UPDATE, [{
45
+ key: ctx.raw.key,
46
+ update: { message: ctx.raw.message },
47
+ ctx
48
+ }])
49
+ }
33
50
  }
34
51
  })
35
52
 
@@ -4,7 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true })
4
4
 
5
5
  class JidNormalizer {
6
6
  /**
7
- * Sanitizes and normalizes any phone number or JID string into a canonical JID
7
+ * Sanitizes and normalizes any phone number or JID string into a canonical JID,
8
+ * stripping device specifiers (e.g., ":1@s.whatsapp.net" -> "@s.whatsapp.net")
8
9
  * @param {string} input - Phone number (e.g., "+55 (11) 99999-9999") or existing JID
9
10
  * @returns {string} Normalized JID (e.g., "5511999999999@s.whatsapp.net")
10
11
  */
@@ -13,7 +14,9 @@ class JidNormalizer {
13
14
  const cleaned = input.trim()
14
15
 
15
16
  if (cleaned.includes('@')) {
16
- return cleaned
17
+ const [userPart, server] = cleaned.split('@')
18
+ const cleanUser = userPart.split(':')[0]
19
+ return `${cleanUser}@${server}`
17
20
  }
18
21
 
19
22
  const digits = cleaned.replace(/\D/g, '')
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true })
4
4
 
5
5
  class MessageNormalizer {
6
6
  /**
7
- * Unwraps container wrappers (ephemeral, viewOnce, documentWithCaption, etc.)
7
+ * Recursively unwraps all container wrappers (ephemeral, viewOnce, associatedChild, etc.)
8
8
  * @param {Object} rawMessageContent - raw message node from proto
9
9
  * @returns {Object} Unwrapped message object
10
10
  */
@@ -12,81 +12,347 @@ class MessageNormalizer {
12
12
  if (!rawMessageContent) return null
13
13
 
14
14
  let msg = rawMessageContent
15
- while (
16
- msg.ephemeralMessage ||
17
- msg.viewOnceMessage ||
18
- msg.viewOnceMessageV2 ||
19
- msg.viewOnceMessageV2Extension ||
20
- msg.documentWithCaptionMessage
21
- ) {
22
- msg =
23
- msg.ephemeralMessage?.message ||
24
- msg.viewOnceMessage?.message ||
25
- msg.viewOnceMessageV2?.message ||
26
- msg.viewOnceMessageV2Extension?.message ||
27
- msg.documentWithCaptionMessage?.message ||
28
- msg
15
+ let unwrapped = true
16
+
17
+ // Loop to peel away multiple layers of wrappers if nested
18
+ while (unwrapped && msg) {
19
+ unwrapped = false
20
+
21
+ if (msg.ephemeralMessage?.message) {
22
+ msg = msg.ephemeralMessage.message
23
+ unwrapped = true
24
+ continue
25
+ }
26
+ if (msg.viewOnceMessage?.message) {
27
+ msg = msg.viewOnceMessage.message
28
+ unwrapped = true
29
+ continue
30
+ }
31
+ if (msg.viewOnceMessageV2?.message) {
32
+ msg = msg.viewOnceMessageV2.message
33
+ unwrapped = true
34
+ continue
35
+ }
36
+ if (msg.viewOnceMessageV2Extension?.message) {
37
+ msg = msg.viewOnceMessageV2Extension.message
38
+ unwrapped = true
39
+ continue
40
+ }
41
+ if (msg.documentWithCaptionMessage?.message) {
42
+ msg = msg.documentWithCaptionMessage.message
43
+ unwrapped = true
44
+ continue
45
+ }
46
+ if (msg.associatedChildMessage?.message) {
47
+ msg = msg.associatedChildMessage.message
48
+ unwrapped = true
49
+ continue
50
+ }
29
51
  }
30
52
 
31
53
  return msg
32
54
  }
33
55
 
34
56
  /**
35
- * Identifies the primary content type of the message
36
- * @param {Object} messageContent - Unwrapped message content
37
- * @returns {string} Normalized type (e.g., 'text', 'image', 'video', 'audio', etc.)
57
+ * Identifies the primary friendly content type of the message (28 distinct types)
58
+ * @param {Object} messageContent - Raw or unwrapped message content
59
+ * @returns {string} Normalized friendly type
38
60
  */
39
61
  static extractType(messageContent) {
40
62
  const msg = this.unwrapMessage(messageContent)
41
63
  if (!msg) return 'unknown'
42
64
 
65
+ // 1. Text & Media Basics
43
66
  if (msg.conversation || msg.extendedTextMessage) return 'text'
44
67
  if (msg.imageMessage) return 'image'
68
+ if (msg.albumMessage) return 'album'
45
69
  if (msg.videoMessage) return msg.videoMessage.gifPlayback ? 'gif' : 'video'
46
70
  if (msg.audioMessage) return msg.audioMessage.ptt ? 'voice' : 'audio'
47
71
  if (msg.documentMessage) return 'document'
48
72
  if (msg.stickerMessage) return 'sticker'
73
+ if (msg.locationMessage) return 'location'
74
+ if (msg.liveLocationMessage) return 'live_location'
49
75
  if (msg.contactMessage || msg.contactsArrayMessage) return 'contact'
50
- if (msg.locationMessage || msg.liveLocationMessage) return 'location'
51
- if (msg.pollCreationMessage || msg.pollCreationMessageV2 || msg.pollCreationMessageV3) return 'poll'
76
+
77
+ // 2. Polls & Events
78
+ if (msg.pollCreationMessage || msg.pollCreationMessageV2 || msg.pollCreationMessageV3) return 'poll_creation'
79
+ if (msg.pollUpdateMessage) return 'poll_update'
80
+ if (msg.eventMessage) return 'event'
81
+ if (msg.eventResponseMessage || msg.encEventResponseMessage) return 'event_response'
82
+
83
+ // 3. Reactions & Pins
52
84
  if (msg.reactionMessage) return 'reaction'
53
- if (msg.interactiveMessage || msg.interactiveResponseMessage) return 'interactive'
54
- if (msg.buttonsResponseMessage || msg.templateButtonReplyMessage) return 'button_reply'
85
+ if (msg.pinInChatMessage) return 'pin_in_chat'
86
+ if (msg.unpinInChatMessage) return 'unpin_in_chat'
87
+
88
+ // 4. Replies from Buttons, Lists & Questions
89
+ if (msg.buttonsResponseMessage) return 'button_reply'
55
90
  if (msg.listResponseMessage) return 'list_reply'
91
+ if (msg.templateButtonReplyMessage) return 'template_button_reply'
92
+ if (msg.questionReplyMessage) return 'question_reply'
93
+
94
+ // 5. Specific Interactive Flow Buttons
95
+ const firstBtnName = msg.interactiveMessage?.interactiveButtons?.[0]?.name
96
+ if (firstBtnName) {
97
+ switch (firstBtnName) {
98
+ case 'review_and_pay': return 'review_and_pay'
99
+ case 'review_order': return 'review_order'
100
+ case 'quick_reply': return 'quick_reply'
101
+ case 'cta_url': return 'cta_url'
102
+ case 'cta_copy': return 'cta_copy'
103
+ case 'cta_call': return 'cta_call'
104
+ case 'voice_call': return 'voice_call'
105
+ case 'payment_info': return 'payment_info'
106
+ }
107
+ }
108
+
109
+ // 6. Native Flow Responses
110
+ if (msg.interactiveResponseMessage?.nativeFlowResponseMessage) {
111
+ const flowName = msg.interactiveResponseMessage.nativeFlowResponseMessage.name
112
+ if (flowName === 'menu_options') return 'native_flow_menu_options_response'
113
+ return 'native_flow_response'
114
+ }
115
+
116
+ // 7. Interactive Containers
117
+ if (msg.interactiveMessage?.interactiveButtons) return 'interactive_buttons'
118
+ if (msg.interactiveMessage?.interactiveList) return 'interactive_list'
119
+ if (msg.interactiveResponseMessage) return 'interactive_response'
120
+ if (msg.interactiveMessage) return 'interactive_message'
121
+
122
+ // 8. Protocol
56
123
  if (msg.protocolMessage) return 'protocol'
57
124
 
125
+ // Fallback to first non-metadata proto key
58
126
  const keys = Object.keys(msg).filter(k => k !== 'messageContextInfo' && k !== 'senderKeyDistributionMessage')
59
127
  return keys[0] ? keys[0].replace('Message', '').toLowerCase() : 'unknown'
60
128
  }
61
129
 
62
130
  /**
63
131
  * Extracts readable text content across diverse message structures
64
- * @param {Object} messageContent - Unwrapped message content
132
+ * @param {Object} messageContent - Raw or unwrapped message content
65
133
  * @returns {string} Text body
66
134
  */
67
135
  static extractBody(messageContent) {
68
136
  const msg = this.unwrapMessage(messageContent)
69
137
  if (!msg) return ''
70
138
 
139
+ // Direct Text
71
140
  if (msg.conversation) return msg.conversation
72
141
  if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text
142
+
143
+ // Media Captions
73
144
  if (msg.imageMessage?.caption) return msg.imageMessage.caption
74
145
  if (msg.videoMessage?.caption) return msg.videoMessage.caption
75
146
  if (msg.documentMessage?.caption) return msg.documentMessage.caption
147
+
148
+ // Button / List selections
76
149
  if (msg.buttonsResponseMessage?.selectedDisplayText) return msg.buttonsResponseMessage.selectedDisplayText
150
+ if (msg.buttonsResponseMessage?.selectedButtonId) return msg.buttonsResponseMessage.selectedButtonId
77
151
  if (msg.listResponseMessage?.title) return msg.listResponseMessage.title
152
+ if (msg.templateButtonReplyMessage?.selectedDisplayText) return msg.templateButtonReplyMessage.selectedDisplayText
78
153
  if (msg.templateButtonReplyMessage?.selectedId) return msg.templateButtonReplyMessage.selectedId
154
+
155
+ // Interactive / Native Flows
156
+ if (msg.interactiveResponseMessage?.body?.text) return msg.interactiveResponseMessage.body.text
79
157
  if (msg.interactiveResponseMessage?.nativeFlowResponseMessage?.paramsJson) {
80
158
  return msg.interactiveResponseMessage.nativeFlowResponseMessage.paramsJson
81
159
  }
160
+ if (msg.interactiveMessage?.body?.text) return msg.interactiveMessage.body.text
161
+
162
+ // Polls, Reactions, Questions & Locations
82
163
  if (msg.pollCreationMessage?.name) return msg.pollCreationMessage.name
83
164
  if (msg.pollCreationMessageV2?.name) return msg.pollCreationMessageV2.name
84
165
  if (msg.pollCreationMessageV3?.name) return msg.pollCreationMessageV3.name
85
166
  if (msg.reactionMessage?.text) return msg.reactionMessage.text
167
+ if (msg.questionReplyMessage?.message?.extendedTextMessage?.text) return msg.questionReplyMessage.message.extendedTextMessage.text
168
+ if (msg.questionReplyMessage?.answer) return msg.questionReplyMessage.answer
169
+ if (msg.locationMessage?.name || msg.locationMessage?.address) {
170
+ return [msg.locationMessage.name, msg.locationMessage.address].filter(Boolean).join(' - ')
171
+ }
86
172
 
87
173
  return ''
88
174
  }
89
175
 
176
+ /**
177
+ * Extracts and safely parses interactive button/list responses
178
+ * @param {Object} messageContent - Unwrapped message content
179
+ * @returns {Object|null} { id, text, params, flowName, type }
180
+ */
181
+ static extractInteractiveReply(messageContent) {
182
+ const msg = this.unwrapMessage(messageContent)
183
+ if (!msg) return null
184
+
185
+ // 1. Classic Button Response
186
+ if (msg.buttonsResponseMessage) {
187
+ return {
188
+ id: msg.buttonsResponseMessage.selectedButtonId || '',
189
+ text: msg.buttonsResponseMessage.selectedDisplayText || '',
190
+ type: 'button'
191
+ }
192
+ }
193
+
194
+ // 2. List Dropdown Selection
195
+ if (msg.listResponseMessage) {
196
+ const row = msg.listResponseMessage.singleSelectReply
197
+ return {
198
+ id: row?.selectedRowId || '',
199
+ text: msg.listResponseMessage.title || '',
200
+ description: row?.description || '',
201
+ type: 'list'
202
+ }
203
+ }
204
+
205
+ // 3. Native Flow Interactive Response
206
+ if (msg.interactiveResponseMessage?.nativeFlowResponseMessage) {
207
+ const flow = msg.interactiveResponseMessage.nativeFlowResponseMessage
208
+ let params = {}
209
+ if (flow.paramsJson) {
210
+ try {
211
+ params = JSON.parse(flow.paramsJson)
212
+ } catch {
213
+ params = { raw: flow.paramsJson }
214
+ }
215
+ }
216
+
217
+ const id = params?.id || params?.selected_id || params?.row_id || flow.name || ''
218
+ return {
219
+ id,
220
+ text: flow.paramsJson || '',
221
+ params,
222
+ flowName: flow.name || '',
223
+ type: 'native_flow'
224
+ }
225
+ }
226
+
227
+ // 4. Template Button Reply
228
+ if (msg.templateButtonReplyMessage) {
229
+ return {
230
+ id: msg.templateButtonReplyMessage.selectedId || '',
231
+ text: msg.templateButtonReplyMessage.selectedDisplayText || '',
232
+ type: 'template_button'
233
+ }
234
+ }
235
+
236
+ // 5. Question Reply (Channels)
237
+ if (msg.questionReplyMessage) {
238
+ return {
239
+ id: msg.questionReplyMessage.selectedButtonId || '',
240
+ text: msg.questionReplyMessage.selectedDisplayText || msg.questionReplyMessage.answer || '',
241
+ type: 'question_reply'
242
+ }
243
+ }
244
+
245
+ return null
246
+ }
247
+
248
+ /**
249
+ * Extracts and parses contacts from vCards
250
+ * @param {Object} messageContent - Unwrapped message content
251
+ * @returns {Array<{ name: string, number: string, org?: string, vcard: string }>}
252
+ */
253
+ static extractContacts(messageContent) {
254
+ const msg = this.unwrapMessage(messageContent)
255
+ if (!msg) return []
256
+
257
+ const parseVcard = (vcard = '') => {
258
+ const nameMatch = vcard.match(/FN:(.+)/i)
259
+ // Tenta waid= primeiro, se não encontrar pega TEL
260
+ const waidMatch = vcard.match(/waid=(\d+)/i)
261
+ const telMatch = vcard.match(/TEL[^:]*:([+\d\s().-]+)/i)
262
+ const orgMatch = vcard.match(/ORG:([^;\n\r]+)/i)
263
+
264
+ let number = ''
265
+ if (waidMatch) {
266
+ number = waidMatch[1].replace(/\D/g, '')
267
+ } else if (telMatch) {
268
+ number = telMatch[1].replace(/\D/g, '')
269
+ }
270
+
271
+ return {
272
+ name: (nameMatch ? nameMatch[1] : '').replace(/\\/g, '').trim(),
273
+ number,
274
+ org: orgMatch ? orgMatch[1].trim() : undefined,
275
+ vcard
276
+ }
277
+ }
278
+
279
+ if (msg.contactMessage) {
280
+ return [parseVcard(msg.contactMessage.vcard)]
281
+ }
282
+
283
+ if (msg.contactsArrayMessage?.contacts) {
284
+ return msg.contactsArrayMessage.contacts.map(c => parseVcard(c.vcard))
285
+ }
286
+
287
+ return []
288
+ }
289
+
290
+ /**
291
+ * Extracts coordinates and metadata from location messages
292
+ * @param {Object} messageContent - Unwrapped message content
293
+ * @returns {Object|null}
294
+ */
295
+ static extractLocation(messageContent) {
296
+ const msg = this.unwrapMessage(messageContent)
297
+ if (!msg) return null
298
+
299
+ const loc = msg.locationMessage || msg.liveLocationMessage
300
+ if (!loc) return null
301
+
302
+ return {
303
+ latitude: Number(loc.degreesLatitude || 0),
304
+ longitude: Number(loc.degreesLongitude || 0),
305
+ name: loc.name || '',
306
+ address: loc.address || '',
307
+ url: loc.url || '',
308
+ accuracyInMeters: loc.accuracyInMeters ? Number(loc.accuracyInMeters) : undefined,
309
+ speedInMps: loc.speedInMps ? Number(loc.speedInMps) : undefined,
310
+ isLive: Boolean(msg.liveLocationMessage)
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Extracts reaction details
316
+ * @param {Object} messageContent - Unwrapped message content
317
+ * @returns {Object|null}
318
+ */
319
+ static extractReaction(messageContent) {
320
+ const msg = this.unwrapMessage(messageContent)
321
+ if (!msg?.reactionMessage) return null
322
+
323
+ const reaction = msg.reactionMessage
324
+ return {
325
+ emoji: reaction.text || '',
326
+ reactedMessageId: reaction.key?.id || '',
327
+ targetKey: reaction.key || null,
328
+ isRemoval: !reaction.text,
329
+ senderTimestamp: reaction.senderTimestampMs ? new Date(Number(reaction.senderTimestampMs)) : new Date()
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Extracts poll creation structure
335
+ * @param {Object} messageContent - Unwrapped message content
336
+ * @param {Object} [rawMessage]
337
+ * @returns {Object|null}
338
+ */
339
+ static extractPollCreation(messageContent, rawMessage) {
340
+ const msg = this.unwrapMessage(messageContent)
341
+ const poll = msg?.pollCreationMessage || msg?.pollCreationMessageV2 || msg?.pollCreationMessageV3
342
+ if (!poll) return null
343
+
344
+ return {
345
+ id: rawMessage?.key?.id || '',
346
+ name: poll.name || '',
347
+ selectableOptionsCount: Number(poll.selectableOptionsCount || 1),
348
+ allowsMultipleAnswers: Number(poll.selectableOptionsCount || 1) > 1,
349
+ options: (poll.options || []).map(opt => ({
350
+ name: opt.optionName || '',
351
+ hash: opt.optionHash ? Buffer.from(opt.optionHash).toString('hex') : null
352
+ }))
353
+ }
354
+ }
355
+
90
356
  /**
91
357
  * Extracts media node if present
92
358
  * @param {Object} messageContent - Unwrapped message content
@@ -106,6 +372,26 @@ class MessageNormalizer {
106
372
  )
107
373
  }
108
374
 
375
+ /**
376
+ * Extracts structured media details
377
+ * @param {Object} messageContent - Unwrapped message content
378
+ * @returns {Object|null}
379
+ */
380
+ static extractMediaDetails(messageContent) {
381
+ const node = this.extractMediaNode(messageContent)
382
+ if (!node) return null
383
+
384
+ return {
385
+ mimetype: node.mimetype || '',
386
+ fileName: node.fileName || '',
387
+ caption: node.caption || '',
388
+ duration: node.seconds ? Number(node.seconds) : null,
389
+ isPtt: Boolean(node.ptt),
390
+ isGif: Boolean(node.gifPlayback),
391
+ isViewOnce: Boolean(node.viewOnce)
392
+ }
393
+ }
394
+
109
395
  /**
110
396
  * Extracts quoted message context info
111
397
  * @param {Object} messageContent - Unwrapped message content
@@ -125,6 +411,8 @@ class MessageNormalizer {
125
411
  msg.buttonsResponseMessage?.contextInfo ||
126
412
  msg.listResponseMessage?.contextInfo ||
127
413
  msg.interactiveResponseMessage?.contextInfo ||
414
+ msg.templateButtonReplyMessage?.contextInfo ||
415
+ msg.messageContextInfo ||
128
416
  null
129
417
  )
130
418
  }
@@ -92,13 +92,66 @@ export declare class CallContext {
92
92
  playAudio(audioPathOrBuffer: string | Buffer): Promise<any>
93
93
  }
94
94
 
95
+ export interface InteractiveReplyData {
96
+ id: string
97
+ text: string
98
+ params?: any
99
+ flowName?: string
100
+ description?: string
101
+ type: 'button' | 'list' | 'native_flow' | 'template_button' | 'question_reply' | string
102
+ }
103
+
104
+ export interface ContactData {
105
+ name: string
106
+ number: string
107
+ org?: string
108
+ vcard: string
109
+ }
110
+
111
+ export interface LocationData {
112
+ latitude: number
113
+ longitude: number
114
+ name: string
115
+ address: string
116
+ url: string
117
+ accuracyInMeters?: number
118
+ speedInMps?: number
119
+ isLive: boolean
120
+ }
121
+
122
+ export interface ReactionData {
123
+ emoji: string
124
+ reactedMessageId: string
125
+ targetKey: any
126
+ isRemoval: boolean
127
+ senderTimestamp: Date
128
+ }
129
+
130
+ export interface PollData {
131
+ id: string
132
+ name: string
133
+ selectableOptionsCount: number
134
+ allowsMultipleAnswers: boolean
135
+ options: Array<{ name: string; hash: string | null }>
136
+ }
137
+
138
+ export interface MediaDetailsData {
139
+ mimetype: string
140
+ fileName: string
141
+ caption: string
142
+ duration: number | null
143
+ isPtt: boolean
144
+ isGif: boolean
145
+ isViewOnce: boolean
146
+ }
147
+
95
148
  export interface QuotedMessageContext {
96
149
  id: string
97
150
  sender: string
98
151
  senderNumber: string
99
152
  type: string
100
153
  body: string
101
- raw: proto.IMessage
154
+ raw: proto.IMessage | any
102
155
  }
103
156
 
104
157
  export declare class MessageContext {
@@ -106,24 +159,48 @@ export declare class MessageContext {
106
159
  raw: proto.IWebMessageInfo
107
160
  id: string
108
161
  chatId: string
162
+ fromMe: boolean
109
163
  isFromMe: boolean
110
164
  isGroup: boolean
111
165
  isChannel: boolean
112
166
  isBroadcast: boolean
167
+
168
+ from: string
169
+ to: string
170
+ fromPushName: string
171
+ fromNumber: string
113
172
  sender: string
114
173
  senderNumber: string
115
174
  senderName: string
175
+
176
+ timestamp: Date
177
+ isEdited: boolean
178
+ editTimestamp: Date | null
179
+
116
180
  type: string
117
181
  body: string
182
+ interactiveReply: InteractiveReplyData | null
183
+ contacts: ContactData[]
184
+ location: LocationData | null
185
+ reaction: ReactionData | null
186
+ poll: PollData | null
187
+
118
188
  mediaNode: any | null
119
189
  hasMedia: boolean
120
190
  media: MediaObject | null
121
- timestamp: number
191
+ mediaDetails: MediaDetailsData | null
192
+
122
193
  contextInfo: proto.IContextInfo | null
194
+ mentions: string[]
123
195
  mentionedJids: string[]
124
- quoted: QuotedMessageContext | null
196
+ isForwarded: boolean
197
+ forwardingScore: number
198
+ isMentioningMe: boolean
199
+
200
+ isReply: boolean
201
+ quoted: MessageContext | QuotedMessageContext | null
125
202
 
126
- constructor(client: WaClient, rawMessage: proto.IWebMessageInfo)
203
+ constructor(client: WaClient, rawMessage: proto.IWebMessageInfo, depth?: number)
127
204
 
128
205
  reply(content: string | AnyMessageContent, options?: MiscMessageGenerationOptions): Promise<proto.WebMessageInfo | undefined>
129
206
  react(emoji: string): Promise<proto.WebMessageInfo | undefined>
@@ -397,7 +474,13 @@ export declare class MessageNormalizer {
397
474
  static unwrapMessage(rawMessageContent: any): any
398
475
  static extractType(messageContent: any): string
399
476
  static extractBody(messageContent: any): string
477
+ static extractInteractiveReply(messageContent: any): InteractiveReplyData | null
478
+ static extractContacts(messageContent: any): ContactData[]
479
+ static extractLocation(messageContent: any): LocationData | null
480
+ static extractReaction(messageContent: any): ReactionData | null
481
+ static extractPollCreation(messageContent: any, rawMessage?: any): PollData | null
400
482
  static extractMediaNode(messageContent: any): any | null
483
+ static extractMediaDetails(messageContent: any): MediaDetailsData | null
401
484
  static extractContextInfo(messageContent: any): proto.IContextInfo | null
402
485
  }
403
486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@areumtecnologia/wasockets",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "whatsappVersion": "2.3000.1053049953",
5
5
  "description": "Communication Library By areumtecnologia",
6
6
  "keywords": [],