@areumtecnologia/wasockets 2.2.3 → 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.
Files changed (31) hide show
  1. package/README.md +98 -25
  2. package/lib/Client/Builders/Buttons.js +127 -0
  3. package/lib/Client/Builders/EventBuilder.js +122 -0
  4. package/lib/Client/Builders/InteractiveMessage.js +150 -0
  5. package/lib/Client/Builders/Lists.js +78 -0
  6. package/lib/Client/Builders/PollBuilder.js +84 -0
  7. package/lib/Client/Builders/index.js +21 -0
  8. package/lib/Client/Constants.js +54 -0
  9. package/lib/Client/Context/CallContext.js +96 -0
  10. package/lib/Client/Context/MediaObject.js +155 -0
  11. package/lib/Client/Context/MessageContext.js +213 -0
  12. package/lib/Client/Modules/BusinessManager.js +155 -0
  13. package/lib/Client/Modules/ChannelManager.js +144 -0
  14. package/lib/Client/Modules/ChatManager.js +113 -0
  15. package/lib/Client/Modules/CommunityManager.js +136 -0
  16. package/lib/Client/Modules/GroupManager.js +194 -0
  17. package/lib/Client/Modules/LabelManager.js +123 -0
  18. package/lib/Client/Modules/MessageManager.js +198 -0
  19. package/lib/Client/Modules/PresenceManager.js +68 -0
  20. package/lib/Client/Modules/PrivacyManager.js +98 -0
  21. package/lib/Client/Modules/ProfileManager.js +92 -0
  22. package/lib/Client/Modules/UserManager.js +92 -0
  23. package/lib/Client/Modules/VoipManager.js +117 -0
  24. package/lib/Client/Modules/index.js +29 -0
  25. package/lib/Client/Normalizers/EventNormalizer.js +99 -0
  26. package/lib/Client/Normalizers/JidNormalizer.js +96 -0
  27. package/lib/Client/Normalizers/MessageNormalizer.js +422 -0
  28. package/lib/Client/WaClient.js +438 -0
  29. package/lib/Client/index.d.ts +547 -0
  30. package/lib/Client/index.js +76 -0
  31. package/package.json +29 -23
@@ -0,0 +1,84 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ /**
6
+ * Fluent builder for WhatsApp Polls
7
+ */
8
+ class PollBuilder {
9
+ constructor() {
10
+ this._name = ''
11
+ this._options = []
12
+ this._selectableCount = 1
13
+ }
14
+
15
+ /**
16
+ * Sets poll question / title
17
+ * @param {string} name
18
+ * @returns {this}
19
+ */
20
+ setName(name) {
21
+ this._name = name || ''
22
+ return this
23
+ }
24
+
25
+ /**
26
+ * Adds an option to the poll
27
+ * @param {string} option
28
+ * @returns {this}
29
+ */
30
+ addOption(option) {
31
+ if (option && typeof option === 'string') {
32
+ this._options.push(option.trim())
33
+ }
34
+ return this
35
+ }
36
+
37
+ /**
38
+ * Adds multiple options to the poll
39
+ * @param {Array<string>} options
40
+ * @returns {this}
41
+ */
42
+ addOptions(options) {
43
+ if (Array.isArray(options)) {
44
+ for (const opt of options) {
45
+ this.addOption(opt)
46
+ }
47
+ }
48
+ return this
49
+ }
50
+
51
+ /**
52
+ * Sets how many options can be selected (1 for single choice, > 1 for multiple choices)
53
+ * @param {number} count
54
+ * @returns {this}
55
+ */
56
+ setSelectableCount(count) {
57
+ this._selectableCount = Math.max(1, Number(count) || 1)
58
+ return this
59
+ }
60
+
61
+ /**
62
+ * Compiles poll into a payload for sendMessage()
63
+ * @returns {Object}
64
+ */
65
+ build() {
66
+ if (!this._name) {
67
+ throw new Error('PollBuilder requires a question name via setName().')
68
+ }
69
+ if (this._options.length < 2) {
70
+ throw new Error('PollBuilder requires at least 2 options.')
71
+ }
72
+
73
+ return {
74
+ poll: {
75
+ name: this._name,
76
+ values: this._options,
77
+ selectableCount: this._selectableCount
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ exports.PollBuilder = PollBuilder
84
+ exports.default = PollBuilder
@@ -0,0 +1,21 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ const { InteractiveMessage } = require("./InteractiveMessage")
6
+ const { QuickReplyButton, UrlButton, CopyCodeButton, CallButton, PixButton } = require("./Buttons")
7
+ const { ListButton, ListSection, ListRow } = require("./Lists")
8
+ const { PollBuilder } = require("./PollBuilder")
9
+ const { EventBuilder } = require("./EventBuilder")
10
+
11
+ exports.InteractiveMessage = InteractiveMessage
12
+ exports.QuickReplyButton = QuickReplyButton
13
+ exports.UrlButton = UrlButton
14
+ exports.CopyCodeButton = CopyCodeButton
15
+ exports.CallButton = CallButton
16
+ exports.PixButton = PixButton
17
+ exports.ListButton = ListButton
18
+ exports.ListSection = ListSection
19
+ exports.ListRow = ListRow
20
+ exports.PollBuilder = PollBuilder
21
+ exports.EventBuilder = EventBuilder
@@ -0,0 +1,54 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ /**
6
+ * High-level semantic event names for WaClient
7
+ */
8
+ const Events = Object.freeze({
9
+ // Connection Lifecycle
10
+ STATUS_CHANGE: 'status.change',
11
+ QR_CODE: 'qr',
12
+ PAIRING_CODE: 'pairing.code',
13
+ CONNECTED: 'connected',
14
+ DISCONNECTED: 'disconnected',
15
+ LOGGED_OUT: 'logged.out',
16
+
17
+ // Messages
18
+ MESSAGE_RECEIVED: 'message.received',
19
+ MESSAGE_SENT: 'message.sent',
20
+ MESSAGE_UPDATE: 'message.update',
21
+ MESSAGE_DELETE: 'message.delete',
22
+
23
+ // Presence & Contacts
24
+ PRESENCE_UPDATE: 'presence.update',
25
+ CONTACTS_UPSERT: 'contacts.upsert',
26
+
27
+ // VoIP & Calls
28
+ CALL_INCOMING: 'call.incoming',
29
+ CALL_STATUS: 'call.status',
30
+
31
+ // Group & Community Events
32
+ GROUP_PARTICIPANTS_UPDATE: 'group.participants.update',
33
+
34
+ // Generic Error
35
+ ERROR: 'error'
36
+ })
37
+
38
+ /**
39
+ * Connection states for WaClient state machine
40
+ */
41
+ const ConnectionStatus = Object.freeze({
42
+ IDLE: 'idle',
43
+ INITIALIZING: 'initializing',
44
+ CONNECTING: 'connecting',
45
+ QR_READY: 'qr_ready',
46
+ PAIRING_READY: 'pairing_ready',
47
+ CONNECTED: 'connected',
48
+ RECONNECTING: 'reconnecting',
49
+ LOGGED_OUT: 'logged_out',
50
+ DISCONNECTED: 'disconnected'
51
+ })
52
+
53
+ exports.Events = Events
54
+ exports.ConnectionStatus = ConnectionStatus
@@ -0,0 +1,96 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ const { JidNormalizer } = require("../Normalizers/JidNormalizer")
6
+
7
+ class CallContext {
8
+ /**
9
+ * @param {import('../WaClient').WaClient} client
10
+ * @param {Object} callData - Inbound call offer or active call metadata
11
+ * @param {Object} [activeCallInstance] - Low-level WebAssembly ActiveCall instance
12
+ */
13
+ constructor(client, callData, activeCallInstance = null) {
14
+ this.client = client
15
+ this.raw = callData || {}
16
+
17
+ this.id = this.raw.id || this.raw.callId || ''
18
+ this.from = this.raw.from || this.raw.chatId || ''
19
+ this.fromNumber = JidNormalizer.toPhoneNumber(this.from)
20
+ this.isVideo = Boolean(this.raw.isVideo)
21
+ this.status = this.raw.status || 'calling'
22
+ this.startTime = this.raw.date ? new Date(this.raw.date).getTime() : Date.now()
23
+ this.instance = activeCallInstance || null
24
+ }
25
+
26
+ _getSock() {
27
+ if (!this.client.sock) {
28
+ throw new Error('Socket is not connected.')
29
+ }
30
+ return this.client.sock
31
+ }
32
+
33
+ /**
34
+ * Atende a chamada recebida via WebAssembly VoIP
35
+ * @param {Object} [options={}]
36
+ */
37
+ async accept(options = {}) {
38
+ const sock = this._getSock()
39
+ if (!sock.acceptCall) {
40
+ throw new Error('VoIP stack is not initialized on the underlying socket.')
41
+ }
42
+
43
+ const callOptions = {
44
+ isVideo: this.isVideo,
45
+ ...options
46
+ }
47
+
48
+ const activeCall = await sock.acceptCall(this.id, this.from, callOptions)
49
+ this.instance = activeCall
50
+ this.status = 'connected'
51
+ return activeCall
52
+ }
53
+
54
+ /**
55
+ * Rejeita a chamada recebida
56
+ * @param {number|string} [reason=0]
57
+ */
58
+ async reject(reason = 0) {
59
+ const sock = this._getSock()
60
+ if (sock.rejectCall) {
61
+ await sock.rejectCall(this.id, this.from, reason)
62
+ }
63
+ this.status = 'rejected'
64
+ }
65
+
66
+ /**
67
+ * Encerra a chamada em andamento
68
+ */
69
+ async hangup() {
70
+ if (this.instance && typeof this.instance.terminate === 'function') {
71
+ await this.instance.terminate()
72
+ } else if (this.instance && typeof this.instance.end === 'function') {
73
+ await this.instance.end()
74
+ } else {
75
+ const sock = this._getSock()
76
+ if (sock.rejectCall) {
77
+ await sock.rejectCall(this.id, this.from, 0)
78
+ }
79
+ }
80
+ this.status = 'ended'
81
+ }
82
+
83
+ /**
84
+ * Transmite áudio na chamada conectada
85
+ * @param {string|Buffer} audioPathOrBuffer
86
+ */
87
+ async playAudio(audioPathOrBuffer) {
88
+ if (this.instance && typeof this.instance.playAudio === 'function') {
89
+ return this.instance.playAudio(audioPathOrBuffer)
90
+ }
91
+ throw new Error('No active audio stream or call is not connected yet.')
92
+ }
93
+ }
94
+
95
+ exports.CallContext = CallContext
96
+ exports.default = CallContext
@@ -0,0 +1,155 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ const { promises: fsPromises } = require("fs")
6
+ const { dirname } = require("path")
7
+ const { downloadContentFromMessage } = require("../../Utils/messages-media")
8
+
9
+ class MediaObject {
10
+ /**
11
+ * @param {Object} mediaNode - Raw protobuf media node (imageMessage, videoMessage, etc.)
12
+ * @param {string} canonicalType - Canonical message type ('image', 'video', 'voice', 'audio', 'document', 'sticker')
13
+ */
14
+ constructor(mediaNode, canonicalType) {
15
+ this.mediaNode = mediaNode || {}
16
+ this.rawType = canonicalType || 'unknown'
17
+ this.mediaType = this._resolveMediaType(canonicalType)
18
+
19
+ this.mimetype = this.mediaNode.mimetype || this._defaultMimetype(this.mediaType)
20
+ this.fileLength = Number(this.mediaNode.fileLength || 0)
21
+ this.fileName = this.mediaNode.fileName || ''
22
+ this.seconds = Number(this.mediaNode.seconds || 0)
23
+ this.isVoice = Boolean(this.mediaNode.ptt)
24
+ this.isAnimated = Boolean(this.mediaNode.gifPlayback || this.mediaNode.isAnimated)
25
+ this.extension = this._inferExtension()
26
+
27
+ this._bufferCache = null
28
+ }
29
+
30
+ _resolveMediaType(type) {
31
+ if (type === 'voice' || type === 'audio') return 'audio'
32
+ if (type === 'gif' || type === 'video') return 'video'
33
+ if (type === 'image') return 'image'
34
+ if (type === 'sticker') return 'sticker'
35
+ if (type === 'document') return 'document'
36
+ return 'document'
37
+ }
38
+
39
+ _defaultMimetype(mediaType) {
40
+ switch (mediaType) {
41
+ case 'image': return 'image/jpeg'
42
+ case 'video': return 'video/mp4'
43
+ case 'audio': return 'audio/ogg; codecs=opus'
44
+ case 'sticker': return 'image/webp'
45
+ default: return 'application/octet-stream'
46
+ }
47
+ }
48
+
49
+ _inferExtension() {
50
+ if (this.fileName && this.fileName.includes('.')) {
51
+ const ext = this.fileName.split('.').pop()
52
+ if (ext) return ext.toLowerCase()
53
+ }
54
+ if (!this.mimetype) return 'bin'
55
+
56
+ const base = this.mimetype.split(';')[0].trim().toLowerCase()
57
+ const sub = base.split('/')[1] || 'bin'
58
+
59
+ if (sub === 'jpeg') return 'jpg'
60
+ if (sub.includes('ogg') || sub.includes('opus')) return 'ogg'
61
+ if (sub === 'pdf') return 'pdf'
62
+ if (sub === 'webp') return 'webp'
63
+ if (sub === 'mp4') return 'mp4'
64
+ if (sub === 'mpeg') return 'mp3'
65
+
66
+ return sub.replace(/[^a-z0-9]/gi, '') || 'bin'
67
+ }
68
+
69
+ /**
70
+ * Downloads media as a Node.js Readable stream
71
+ * @param {Object} [options={}]
72
+ * @returns {Promise<import('stream').Readable>}
73
+ */
74
+ async toStream(options = {}) {
75
+ if (!this.mediaNode.mediaKey && !this.mediaNode.directPath && !this.mediaNode.url) {
76
+ throw new Error('Media node does not contain valid decryption keys or download URL.')
77
+ }
78
+
79
+ return downloadContentFromMessage(this.mediaNode, this.mediaType, options)
80
+ }
81
+
82
+ /**
83
+ * Downloads media and returns decrypted Buffer (caches in memory)
84
+ * @param {Object} [options={}]
85
+ * @returns {Promise<Buffer>}
86
+ */
87
+ async toBuffer(options = {}) {
88
+ if (this._bufferCache) {
89
+ return this._bufferCache
90
+ }
91
+
92
+ const stream = await this.toStream(options)
93
+ const chunks = []
94
+
95
+ for await (const chunk of stream) {
96
+ chunks.push(chunk)
97
+ }
98
+
99
+ this._bufferCache = Buffer.concat(chunks)
100
+ return this._bufferCache
101
+ }
102
+
103
+ /**
104
+ * Saves the downloaded media directly to disk
105
+ * @param {string} destinationPath - Full or relative file path to save
106
+ * @param {Object} [options={}]
107
+ * @returns {Promise<string>} Saved destination path
108
+ */
109
+ async save(destinationPath, options = {}) {
110
+ const buffer = await this.toBuffer(options)
111
+ const dir = dirname(destinationPath)
112
+ await fsPromises.mkdir(dir, { recursive: true })
113
+ await fsPromises.writeFile(destinationPath, buffer)
114
+ return destinationPath
115
+ }
116
+
117
+ /**
118
+ * Returns Base64 encoded string of the media
119
+ * @param {Object} [options={}]
120
+ * @returns {Promise<string>}
121
+ */
122
+ async toBase64(options = {}) {
123
+ const buffer = await this.toBuffer(options)
124
+ return buffer.toString('base64')
125
+ }
126
+
127
+ /**
128
+ * Returns a Data URI string (e.g. data:image/jpeg;base64,...)
129
+ * @param {Object} [options={}]
130
+ * @returns {Promise<string>}
131
+ */
132
+ async toDataUri(options = {}) {
133
+ const base64 = await this.toBase64(options)
134
+ return `data:${this.mimetype};base64,${base64}`
135
+ }
136
+
137
+ /**
138
+ * Returns a Sharp instance for image processing (requires sharp installed)
139
+ * @param {Object} [options={}]
140
+ */
141
+ async toSharp(options = {}) {
142
+ let sharp
143
+ try {
144
+ sharp = require('sharp')
145
+ } catch {
146
+ throw new Error('The "sharp" package is required to use toSharp(). Run "npm install sharp" to add it.')
147
+ }
148
+
149
+ const buffer = await this.toBuffer(options)
150
+ return sharp(buffer)
151
+ }
152
+ }
153
+
154
+ exports.MediaObject = MediaObject
155
+ exports.default = MediaObject
@@ -0,0 +1,213 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ const { JidNormalizer } = require("../Normalizers/JidNormalizer")
6
+ const { MessageNormalizer } = require("../Normalizers/MessageNormalizer")
7
+ const { MediaObject } = require("./MediaObject")
8
+
9
+ class MessageContext {
10
+ /**
11
+ * @param {import('../WaClient').WaClient} client
12
+ * @param {Object} rawMessage - proto.IWebMessageInfo
13
+ * @param {number} [depth=0] - Recursion depth guard for quoted messages
14
+ */
15
+ constructor(client, rawMessage, depth = 0) {
16
+ this.client = client
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
34
+
35
+ this.isGroup = JidNormalizer.isGroup(this.chatId)
36
+ this.isChannel = JidNormalizer.isNewsletter(this.chatId)
37
+ this.isBroadcast = JidNormalizer.isBroadcast(this.chatId)
38
+
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)
80
+ this.hasMedia = Boolean(this.mediaNode)
81
+ this.media = this.hasMedia ? new MediaObject(this.mediaNode, this.type) : null
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
+ )
96
+
97
+ // 6. Quoted Message Parsing (Recursion with depth limit 1)
98
+ this.isReply = Boolean(this.contextInfo?.quotedMessage)
99
+ this.quoted = null
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
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Responde citando esta mensagem automaticamente
130
+ * @param {string|Object} content - Texto ou payload de mensagem
131
+ * @param {Object} [options={}]
132
+ */
133
+ async reply(content, options = {}) {
134
+ const payload = typeof content === 'string' ? { text: content } : content
135
+ return this.client.sendMessage(this.chatId, payload, {
136
+ quoted: this.raw,
137
+ ...options
138
+ })
139
+ }
140
+
141
+ /**
142
+ * Reage à mensagem com um emoji (ou string vazia para remover)
143
+ * @param {string} emoji
144
+ */
145
+ async react(emoji) {
146
+ return this.client.sendMessage(this.chatId, {
147
+ react: {
148
+ text: emoji || '',
149
+ key: this.raw.key
150
+ }
151
+ })
152
+ }
153
+
154
+ /**
155
+ * Remove qualquer reação anteriormente atribuída a esta mensagem
156
+ */
157
+ async removeReaction() {
158
+ return this.react('')
159
+ }
160
+
161
+ /**
162
+ * Apaga esta mensagem para todos na conversa (revoke)
163
+ */
164
+ async delete() {
165
+ return this.client.sendMessage(this.chatId, {
166
+ delete: this.raw.key
167
+ })
168
+ }
169
+
170
+ /**
171
+ * Marca esta mensagem como lida (ticks azuis)
172
+ */
173
+ async markRead() {
174
+ if (!this.client?.sock?.readMessages) return
175
+ return this.client.sock.readMessages([this.raw.key])
176
+ }
177
+
178
+ /**
179
+ * Encaminha esta mensagem para outro destinatário
180
+ * @param {string} targetJid
181
+ * @param {Object} [options={}]
182
+ */
183
+ async forward(targetJid, options = {}) {
184
+ return this.client.sendMessage(
185
+ targetJid,
186
+ { forward: this.raw, ...options }
187
+ )
188
+ }
189
+
190
+ /**
191
+ * Fixa a mensagem no chat com duração configurável
192
+ * @param {number} [durationInSeconds=86400]
193
+ */
194
+ async pin(durationInSeconds = 86400) {
195
+ if (!this.client?.sock?.pinInChatMessage) {
196
+ throw new Error('Método pinInChatMessage indisponível no socket.')
197
+ }
198
+ return this.client.sock.pinInChatMessage(this.chatId, this.raw.key, 1, durationInSeconds)
199
+ }
200
+
201
+ /**
202
+ * Desafixa a mensagem do topo da conversa
203
+ */
204
+ async unpin() {
205
+ if (!this.client?.sock?.pinInChatMessage) {
206
+ throw new Error('Método pinInChatMessage indisponível no socket.')
207
+ }
208
+ return this.client.sock.pinInChatMessage(this.chatId, this.raw.key, 2)
209
+ }
210
+ }
211
+
212
+ exports.MessageContext = MessageContext
213
+ exports.default = MessageContext