@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.
- package/README.md +98 -25
- package/lib/Client/Builders/Buttons.js +127 -0
- package/lib/Client/Builders/EventBuilder.js +122 -0
- package/lib/Client/Builders/InteractiveMessage.js +150 -0
- package/lib/Client/Builders/Lists.js +78 -0
- package/lib/Client/Builders/PollBuilder.js +84 -0
- package/lib/Client/Builders/index.js +21 -0
- package/lib/Client/Constants.js +54 -0
- package/lib/Client/Context/CallContext.js +96 -0
- package/lib/Client/Context/MediaObject.js +155 -0
- package/lib/Client/Context/MessageContext.js +213 -0
- package/lib/Client/Modules/BusinessManager.js +155 -0
- package/lib/Client/Modules/ChannelManager.js +144 -0
- package/lib/Client/Modules/ChatManager.js +113 -0
- package/lib/Client/Modules/CommunityManager.js +136 -0
- package/lib/Client/Modules/GroupManager.js +194 -0
- package/lib/Client/Modules/LabelManager.js +123 -0
- package/lib/Client/Modules/MessageManager.js +198 -0
- package/lib/Client/Modules/PresenceManager.js +68 -0
- package/lib/Client/Modules/PrivacyManager.js +98 -0
- package/lib/Client/Modules/ProfileManager.js +92 -0
- package/lib/Client/Modules/UserManager.js +92 -0
- package/lib/Client/Modules/VoipManager.js +117 -0
- package/lib/Client/Modules/index.js +29 -0
- package/lib/Client/Normalizers/EventNormalizer.js +99 -0
- package/lib/Client/Normalizers/JidNormalizer.js +96 -0
- package/lib/Client/Normalizers/MessageNormalizer.js +422 -0
- package/lib/Client/WaClient.js +438 -0
- package/lib/Client/index.d.ts +547 -0
- package/lib/Client/index.js +76 -0
- 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
|