@areumtecnologia/wasockets 2.3.0 → 2.3.2

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`:
@@ -19,8 +19,14 @@ class MediaObject {
19
19
  this.mimetype = this.mediaNode.mimetype || this._defaultMimetype(this.mediaType)
20
20
  this.fileLength = Number(this.mediaNode.fileLength || 0)
21
21
  this.fileName = this.mediaNode.fileName || ''
22
+ this.caption = this.mediaNode.caption || ''
22
23
  this.seconds = Number(this.mediaNode.seconds || 0)
24
+ this.duration = this.seconds || null
25
+
23
26
  this.isVoice = Boolean(this.mediaNode.ptt)
27
+ this.isPtt = this.isVoice
28
+ this.isGif = Boolean(this.mediaNode.gifPlayback)
29
+ this.isViewOnce = Boolean(this.mediaNode.viewOnce)
24
30
  this.isAnimated = Boolean(this.mediaNode.gifPlayback || this.mediaNode.isAnimated)
25
31
  this.extension = this._inferExtension()
26
32
 
@@ -10,58 +10,173 @@ 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. Single-pass message unwrapping (eliminates repeated while-loops)
30
+ const unwrapped = MessageNormalizer.unwrapMessage(effectiveMessage)
31
+
32
+ // 3. Identity and Routing
33
+ this.id = effectiveKey.id || rawMessage?.key?.id || ''
34
+ this.chatId = effectiveKey.remoteJid || rawMessage?.key?.remoteJid || ''
35
+ this.fromMe = Boolean(effectiveKey.fromMe)
17
36
 
18
- const key = rawMessage.key || {}
19
- this.id = key.id || ''
20
- this.chatId = key.remoteJid || ''
21
- this.isFromMe = Boolean(key.fromMe)
22
37
  this.isGroup = JidNormalizer.isGroup(this.chatId)
23
38
  this.isChannel = JidNormalizer.isNewsletter(this.chatId)
24
39
  this.isBroadcast = JidNormalizer.isBroadcast(this.chatId)
25
40
 
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)
38
- this.hasMedia = Boolean(this.mediaNode)
39
- 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 || []
45
-
46
- // Quoted message parsing
41
+ // Bot JID resolution
42
+ const sockUser = client?.sock?.user || {}
43
+ const clientJid = sockUser.id ? JidNormalizer.toJid(sockUser.id) : ''
44
+ const clientLid = sockUser.lid ? JidNormalizer.toJid(sockUser.lid) : ''
45
+
46
+ // Origin and Destination (from / to)
47
+ if (this.fromMe) {
48
+ this.from = clientJid || this.chatId
49
+ this.to = this.chatId
50
+ this.fromPushName = sockUser.name || sockUser.notify || 'Você'
51
+ } else {
52
+ this.from = this.isGroup
53
+ ? (effectiveKey.participant || rawMessage?.participant || this.chatId)
54
+ : this.chatId
55
+ this.to = clientJid || this.chatId
56
+ this.fromPushName = rawMessage?.pushName || JidNormalizer.toPhoneNumber(this.from) || 'Desconhecido'
57
+ }
58
+
59
+ this.fromNumber = JidNormalizer.toPhoneNumber(this.from)
60
+
61
+ // Timestamps
62
+ const rawTs = Number(rawMessage?.messageTimestamp || Math.floor(Date.now() / 1000))
63
+ this.timestamp = new Date(rawTs > 10000000000 ? rawTs : rawTs * 1000)
64
+ this.editTimestamp = this.isEdited
65
+ ? new Date(Number(rawMessage.message.protocolMessage.timestampMs || Date.now()))
66
+ : null
67
+
68
+ // 4. Content Decoupling & Typed Parsers (Single-pass unwrapped nodes)
69
+ this.type = MessageNormalizer.extractType(unwrapped)
70
+ this.body = MessageNormalizer.extractBody(unwrapped)
71
+ this.interactiveReply = MessageNormalizer.extractInteractiveReply(unwrapped)
72
+ this.contacts = MessageNormalizer.extractContacts(unwrapped)
73
+ this.location = MessageNormalizer.extractLocation(unwrapped)
74
+ this.reaction = MessageNormalizer.extractReaction(unwrapped)
75
+ this.poll = MessageNormalizer.extractPollCreation(unwrapped, rawMessage)
76
+
77
+ // 5. Media Normalization (Centralized directly into MediaObject)
78
+ this.mediaNode = MessageNormalizer.extractMediaNode(unwrapped)
79
+ this.media = this.mediaNode ? new MediaObject(this.mediaNode, this.type) : null
80
+
81
+ // 6. Context Info, Mentions & Forwarding
82
+ this.contextInfo = MessageNormalizer.extractContextInfo(unwrapped)
83
+ this.mentions = this.contextInfo?.mentionedJid || []
84
+ this.isForwarded = Boolean(this.contextInfo?.isForwarded)
85
+ this.forwardingScore = Number(this.contextInfo?.forwardingScore || 0)
86
+
87
+ // Checks if the bot was directly mentioned (by PN or LID)
88
+ this.isMentioningMe = Boolean(
89
+ (clientJid && this.mentions.includes(clientJid)) ||
90
+ (clientLid && this.mentions.includes(clientLid))
91
+ )
92
+
93
+ // 7. Quoted Message Parsing (Recursion with depth limit 1)
94
+ this.isReply = Boolean(this.contextInfo?.quotedMessage)
47
95
  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
96
+
97
+ if (this.isReply && this.contextInfo?.quotedMessage && depth === 0) {
98
+ const quotedParticipant = this.contextInfo.participant || this.chatId
99
+ const quotedRaw = {
100
+ key: {
101
+ remoteJid: this.chatId,
102
+ id: this.contextInfo.stanzaId,
103
+ fromMe: clientJid && JidNormalizer.toJid(quotedParticipant) === clientJid,
104
+ participant: quotedParticipant
105
+ },
106
+ message: this.contextInfo.quotedMessage,
107
+ pushName: this.contextInfo.pushName
108
+ }
109
+ try {
110
+ this.quoted = new MessageContext(this.client, quotedRaw, depth + 1)
111
+ } catch {
112
+ this.quoted = {
113
+ id: this.contextInfo.stanzaId,
114
+ sender: quotedParticipant,
115
+ senderNumber: JidNormalizer.toPhoneNumber(quotedParticipant),
116
+ type: MessageNormalizer.extractType(this.contextInfo.quotedMessage),
117
+ body: MessageNormalizer.extractBody(this.contextInfo.quotedMessage),
118
+ raw: this.contextInfo.quotedMessage
119
+ }
57
120
  }
58
121
  }
59
122
  }
60
123
 
124
+ // ==========================================
125
+ // Zero-Cost Memory Aliases & Compatibility Getters
126
+ // ==========================================
127
+
128
+ /** Alias for this.fromMe */
129
+ get isFromMe() {
130
+ return this.fromMe
131
+ }
132
+
133
+ /** Alias for this.from */
134
+ get sender() {
135
+ return this.from
136
+ }
137
+
138
+ /** Alias for this.fromNumber */
139
+ get senderNumber() {
140
+ return this.fromNumber
141
+ }
142
+
143
+ /** Alias for this.fromPushName */
144
+ get senderName() {
145
+ return this.fromPushName
146
+ }
147
+
148
+ /** Alias for this.mentions */
149
+ get mentionedJids() {
150
+ return this.mentions
151
+ }
152
+
153
+ /** Boolean check whether message contains media */
154
+ get hasMedia() {
155
+ return Boolean(this.media)
156
+ }
157
+
158
+ /** Backward-compatible media details accessor backed by MediaObject */
159
+ get mediaDetails() {
160
+ if (!this.media) return null
161
+ return {
162
+ mimetype: this.media.mimetype,
163
+ fileName: this.media.fileName,
164
+ caption: this.media.caption,
165
+ duration: this.media.duration,
166
+ isPtt: this.media.isPtt,
167
+ isGif: this.media.isGif,
168
+ isViewOnce: this.media.isViewOnce
169
+ }
170
+ }
171
+
172
+ // ==========================================
173
+ // Context Action Methods
174
+ // ==========================================
175
+
61
176
  /**
62
177
  * 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
178
+ * @param {string|Object} content - Texto ou payload de mensagem
179
+ * @param {Object} [options={}]
65
180
  */
66
181
  async reply(content, options = {}) {
67
182
  const payload = typeof content === 'string' ? { text: content } : content
@@ -72,8 +187,8 @@ class MessageContext {
72
187
  }
73
188
 
74
189
  /**
75
- * Reage à mensagem com um emoji
76
- * @param {string} emoji - Emoji único (ex: '👍', '❤️', '🔥')
190
+ * Reage à mensagem com um emoji (ou string vazia para remover)
191
+ * @param {string} emoji
77
192
  */
78
193
  async react(emoji) {
79
194
  return this.client.sendMessage(this.chatId, {
@@ -104,13 +219,13 @@ class MessageContext {
104
219
  * Marca esta mensagem como lida (ticks azuis)
105
220
  */
106
221
  async markRead() {
107
- if (!this.client.sock?.readMessages) return
222
+ if (!this.client?.sock?.readMessages) return
108
223
  return this.client.sock.readMessages([this.raw.key])
109
224
  }
110
225
 
111
226
  /**
112
227
  * Encaminha esta mensagem para outro destinatário
113
- * @param {string} targetJid - Destinatário
228
+ * @param {string} targetJid
114
229
  * @param {Object} [options={}]
115
230
  */
116
231
  async forward(targetJid, options = {}) {
@@ -122,10 +237,10 @@ class MessageContext {
122
237
 
123
238
  /**
124
239
  * Fixa a mensagem no chat com duração configurável
125
- * @param {number} [durationInSeconds=86400] - 86400 (24h), 604800 (7d), 2592000 (30d)
240
+ * @param {number} [durationInSeconds=86400]
126
241
  */
127
242
  async pin(durationInSeconds = 86400) {
128
- if (!this.client.sock?.pinInChatMessage) {
243
+ if (!this.client?.sock?.pinInChatMessage) {
129
244
  throw new Error('Método pinInChatMessage indisponível no socket.')
130
245
  }
131
246
  return this.client.sock.pinInChatMessage(this.chatId, this.raw.key, 1, durationInSeconds)
@@ -135,7 +250,7 @@ class MessageContext {
135
250
  * Desafixa a mensagem do topo da conversa
136
251
  */
137
252
  async unpin() {
138
- if (!this.client.sock?.pinInChatMessage) {
253
+ if (!this.client?.sock?.pinInChatMessage) {
139
254
  throw new Error('Método pinInChatMessage indisponível no socket.')
140
255
  }
141
256
  return this.client.sock.pinInChatMessage(this.chatId, this.raw.key, 2)
@@ -20,9 +20,18 @@ 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 (delete for everyone)
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
+ continue // Do not emit as a new received/sent message
33
+ }
34
+
26
35
  const ctx = new MessageContext(client, rawMsg)
27
36
 
28
37
  if (ctx.isFromMe) {
@@ -30,6 +39,15 @@ class EventNormalizer {
30
39
  } else {
31
40
  client.emit(Events.MESSAGE_RECEIVED, ctx, rawMsg)
32
41
  }
42
+
43
+ // If message is an edit, also notify on MESSAGE_UPDATE
44
+ if (ctx.isEdited) {
45
+ client.emit(Events.MESSAGE_UPDATE, [{
46
+ key: ctx.raw.key,
47
+ update: { message: ctx.raw.message },
48
+ ctx
49
+ }])
50
+ }
33
51
  }
34
52
  })
35
53
 
@@ -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
  }
@@ -58,8 +58,13 @@ export declare class MediaObject {
58
58
  mimetype: string
59
59
  fileLength: number
60
60
  fileName: string
61
+ caption: string
61
62
  seconds: number
63
+ duration: number | null
62
64
  isVoice: boolean
65
+ isPtt: boolean
66
+ isGif: boolean
67
+ isViewOnce: boolean
63
68
  isAnimated: boolean
64
69
  extension: string
65
70
 
@@ -92,13 +97,66 @@ export declare class CallContext {
92
97
  playAudio(audioPathOrBuffer: string | Buffer): Promise<any>
93
98
  }
94
99
 
100
+ export interface InteractiveReplyData {
101
+ id: string
102
+ text: string
103
+ params?: any
104
+ flowName?: string
105
+ description?: string
106
+ type: 'button' | 'list' | 'native_flow' | 'template_button' | 'question_reply' | string
107
+ }
108
+
109
+ export interface ContactData {
110
+ name: string
111
+ number: string
112
+ org?: string
113
+ vcard: string
114
+ }
115
+
116
+ export interface LocationData {
117
+ latitude: number
118
+ longitude: number
119
+ name: string
120
+ address: string
121
+ url: string
122
+ accuracyInMeters?: number
123
+ speedInMps?: number
124
+ isLive: boolean
125
+ }
126
+
127
+ export interface ReactionData {
128
+ emoji: string
129
+ reactedMessageId: string
130
+ targetKey: any
131
+ isRemoval: boolean
132
+ senderTimestamp: Date
133
+ }
134
+
135
+ export interface PollData {
136
+ id: string
137
+ name: string
138
+ selectableOptionsCount: number
139
+ allowsMultipleAnswers: boolean
140
+ options: Array<{ name: string; hash: string | null }>
141
+ }
142
+
143
+ export interface MediaDetailsData {
144
+ mimetype: string
145
+ fileName: string
146
+ caption: string
147
+ duration: number | null
148
+ isPtt: boolean
149
+ isGif: boolean
150
+ isViewOnce: boolean
151
+ }
152
+
95
153
  export interface QuotedMessageContext {
96
154
  id: string
97
155
  sender: string
98
156
  senderNumber: string
99
157
  type: string
100
158
  body: string
101
- raw: proto.IMessage
159
+ raw: proto.IMessage | any
102
160
  }
103
161
 
104
162
  export declare class MessageContext {
@@ -106,24 +164,48 @@ export declare class MessageContext {
106
164
  raw: proto.IWebMessageInfo
107
165
  id: string
108
166
  chatId: string
167
+ fromMe: boolean
109
168
  isFromMe: boolean
110
169
  isGroup: boolean
111
170
  isChannel: boolean
112
171
  isBroadcast: boolean
172
+
173
+ from: string
174
+ to: string
175
+ fromPushName: string
176
+ fromNumber: string
113
177
  sender: string
114
178
  senderNumber: string
115
179
  senderName: string
180
+
181
+ timestamp: Date
182
+ isEdited: boolean
183
+ editTimestamp: Date | null
184
+
116
185
  type: string
117
186
  body: string
187
+ interactiveReply: InteractiveReplyData | null
188
+ contacts: ContactData[]
189
+ location: LocationData | null
190
+ reaction: ReactionData | null
191
+ poll: PollData | null
192
+
118
193
  mediaNode: any | null
119
194
  hasMedia: boolean
120
195
  media: MediaObject | null
121
- timestamp: number
196
+ mediaDetails: MediaDetailsData | null
197
+
122
198
  contextInfo: proto.IContextInfo | null
199
+ mentions: string[]
123
200
  mentionedJids: string[]
124
- quoted: QuotedMessageContext | null
201
+ isForwarded: boolean
202
+ forwardingScore: number
203
+ isMentioningMe: boolean
204
+
205
+ isReply: boolean
206
+ quoted: MessageContext | QuotedMessageContext | null
125
207
 
126
- constructor(client: WaClient, rawMessage: proto.IWebMessageInfo)
208
+ constructor(client: WaClient, rawMessage: proto.IWebMessageInfo, depth?: number)
127
209
 
128
210
  reply(content: string | AnyMessageContent, options?: MiscMessageGenerationOptions): Promise<proto.WebMessageInfo | undefined>
129
211
  react(emoji: string): Promise<proto.WebMessageInfo | undefined>
@@ -397,7 +479,13 @@ export declare class MessageNormalizer {
397
479
  static unwrapMessage(rawMessageContent: any): any
398
480
  static extractType(messageContent: any): string
399
481
  static extractBody(messageContent: any): string
482
+ static extractInteractiveReply(messageContent: any): InteractiveReplyData | null
483
+ static extractContacts(messageContent: any): ContactData[]
484
+ static extractLocation(messageContent: any): LocationData | null
485
+ static extractReaction(messageContent: any): ReactionData | null
486
+ static extractPollCreation(messageContent: any, rawMessage?: any): PollData | null
400
487
  static extractMediaNode(messageContent: any): any | null
488
+ static extractMediaDetails(messageContent: any): MediaDetailsData | null
401
489
  static extractContextInfo(messageContent: any): proto.IContextInfo | null
402
490
  }
403
491
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@areumtecnologia/wasockets",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "whatsappVersion": "2.3000.1053049953",
5
5
  "description": "Communication Library By areumtecnologia",
6
6
  "keywords": [],