@nexustechpro/baileys 2.0.5 → 2.0.6
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/WAProto/index.js +22 -18
- package/lib/Defaults/baileys-version.json +1 -1
- package/lib/Defaults/index.js +7 -6
- package/lib/Signal/libsignal.js +65 -50
- package/lib/Socket/chats.js +64 -57
- package/lib/Socket/index.js +2 -3
- package/lib/Socket/messages-recv.js +227 -41
- package/lib/Socket/messages-send.js +79 -117
- package/lib/Socket/nexus-handler.js +325 -90
- package/lib/Socket/registration.js +50 -33
- package/lib/Socket/socket.js +232 -69
- package/lib/Types/Newsletter.js +37 -29
- package/lib/Types/State.js +43 -0
- package/lib/Utils/auth-utils.js +2 -2
- package/lib/Utils/chat-utils.js +48 -16
- package/lib/Utils/companion-reg-client-utils.js +34 -0
- package/lib/Utils/decode-wa-message.js +40 -8
- package/lib/Utils/generics.js +5 -7
- package/lib/Utils/index.js +4 -0
- package/lib/Utils/link-preview.js +10 -0
- package/lib/Utils/messages-media.js +426 -382
- package/lib/Utils/messages.js +602 -487
- package/lib/Utils/process-message.js +53 -35
- package/lib/Utils/reporting-utils.js +155 -0
- package/lib/Utils/signal.js +134 -104
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +162 -0
- package/lib/WABinary/constants.js +6 -0
- package/lib/WABinary/index.js +1 -0
- package/lib/index.js +2 -3
- package/package.json +6 -4
package/lib/Utils/messages.js
CHANGED
|
@@ -1,108 +1,141 @@
|
|
|
1
|
-
import { Boom } from '@hapi/boom'
|
|
2
|
-
import { randomBytes } from 'crypto'
|
|
3
|
-
import { promises as fs } from 'fs'
|
|
4
|
-
import { zip } from 'fflate'
|
|
5
|
-
import { proto } from '../../WAProto/index.js'
|
|
6
|
-
import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
|
|
7
|
-
import { WAMessageStatus, WAProto } from '../Types/index.js'
|
|
8
|
-
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js'
|
|
9
|
-
import { sha256 } from './crypto.js'
|
|
10
|
-
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js'
|
|
11
|
-
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { randomBytes } from 'crypto'
|
|
3
|
+
import { promises as fs } from 'fs'
|
|
4
|
+
import { zip } from 'fflate'
|
|
5
|
+
import { proto } from '../../WAProto/index.js'
|
|
6
|
+
import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
|
|
7
|
+
import { WAMessageStatus, WAProto } from '../Types/index.js'
|
|
8
|
+
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js'
|
|
9
|
+
import { sha256 } from './crypto.js'
|
|
10
|
+
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js'
|
|
11
|
+
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js'
|
|
12
|
+
import { shouldIncludeReportingToken } from './reporting-utils.js'
|
|
13
|
+
|
|
14
|
+
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
|
15
|
+
const MIMETYPE_MAP = {
|
|
16
|
+
image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf',
|
|
17
|
+
audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MessageTypeProto = {
|
|
21
|
+
image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage,
|
|
22
|
+
audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage,
|
|
23
|
+
document: WAProto.Message.DocumentMessage
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// High-level keys that require processing — NOT raw WAProto passthrough
|
|
27
|
+
const HIGH_LEVEL_KEYS = [
|
|
28
|
+
'text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location',
|
|
29
|
+
'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack',
|
|
30
|
+
'pin', 'buttonReply', 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin',
|
|
31
|
+
'requestPayment', 'sharePhoneNumber', 'requestPhoneNumber', 'limitSharing', 'viewOnce',
|
|
32
|
+
'mentions', 'edit', 'buttons', 'templateButtons', 'sections', 'interactiveButtons',
|
|
33
|
+
'album', 'call', 'paymentInvite', 'order', 'keep', 'shop', 'payment'
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
// ─── UTILITIES ────────────────────────────────────────────────────────────────
|
|
37
|
+
export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0]
|
|
21
38
|
|
|
22
39
|
export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
|
|
23
|
-
const url = extractUrlFromText(text)
|
|
24
|
-
if (!getUrlInfo || !url) return
|
|
25
|
-
try { return await getUrlInfo(url)
|
|
26
|
-
catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed')
|
|
27
|
-
}
|
|
40
|
+
const url = extractUrlFromText(text)
|
|
41
|
+
if (!getUrlInfo || !url) return
|
|
42
|
+
try { return await getUrlInfo(url) }
|
|
43
|
+
catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed') }
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
const assertColor = (color) => {
|
|
30
|
-
if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1
|
|
31
|
-
|
|
32
|
-
return parseInt(
|
|
33
|
-
}
|
|
47
|
+
if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1
|
|
48
|
+
const hex = color.trim().replace('#', '')
|
|
49
|
+
return parseInt(hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex, 16)
|
|
50
|
+
}
|
|
34
51
|
|
|
35
52
|
export const getContentType = (content) => {
|
|
36
|
-
if (!content) return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
53
|
+
if (!content) return
|
|
54
|
+
return Object.keys(content).find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage')
|
|
55
|
+
}
|
|
40
56
|
|
|
41
57
|
export const normalizeMessageContent = (content) => {
|
|
42
|
-
if (!content) return
|
|
58
|
+
if (!content) return
|
|
43
59
|
for (let i = 0; i < 5; i++) {
|
|
44
|
-
const inner =
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
const inner = (
|
|
61
|
+
content?.ephemeralMessage || content?.viewOnceMessage ||
|
|
62
|
+
content?.documentWithCaptionMessage || content?.viewOnceMessageV2 ||
|
|
63
|
+
content?.viewOnceMessageV2Extension || content?.editedMessage ||
|
|
64
|
+
content?.groupMentionedMessage || content?.botInvokeMessage ||
|
|
65
|
+
content?.lottieStickerMessage || content?.eventCoverImage ||
|
|
66
|
+
content?.statusMentionMessage || content?.pollCreationOptionImageMessage ||
|
|
67
|
+
content?.associatedChildMessage || content?.groupStatusMentionMessage ||
|
|
68
|
+
content?.pollCreationMessageV4 || content?.pollCreationMessageV5 ||
|
|
69
|
+
content?.statusAddYours || content?.groupStatusMessage ||
|
|
70
|
+
content?.limitSharingMessage || content?.botTaskMessage ||
|
|
71
|
+
content?.questionMessage || content?.groupStatusMessageV2 ||
|
|
72
|
+
content?.botForwardedMessage
|
|
73
|
+
)
|
|
74
|
+
if (!inner) break
|
|
75
|
+
content = inner.message
|
|
47
76
|
}
|
|
48
|
-
return content
|
|
49
|
-
}
|
|
77
|
+
return content
|
|
78
|
+
}
|
|
50
79
|
|
|
51
80
|
export const extractMessageContent = (content) => {
|
|
52
|
-
content = normalizeMessageContent(content)
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
}
|
|
81
|
+
content = normalizeMessageContent(content)
|
|
82
|
+
const extractFromButtons = (msg) => {
|
|
83
|
+
const header = typeof msg.header === 'object' && msg.header !== null
|
|
84
|
+
if ((header ? msg.header?.imageMessage : msg.imageMessage)) return { imageMessage: header ? msg.header.imageMessage : msg.imageMessage }
|
|
85
|
+
if ((header ? msg.header?.documentMessage : msg.documentMessage)) return { documentMessage: header ? msg.header.documentMessage : msg.documentMessage }
|
|
86
|
+
if ((header ? msg.header?.videoMessage : msg.videoMessage)) return { videoMessage: header ? msg.header.videoMessage : msg.videoMessage }
|
|
87
|
+
if ((header ? msg.header?.locationMessage : msg.locationMessage)) return { locationMessage: header ? msg.header.locationMessage : msg.locationMessage }
|
|
88
|
+
if ((header ? msg.header?.productMessage : msg.productMessage)) return { productMessage: header ? msg.header.productMessage : msg.productMessage }
|
|
89
|
+
return { conversation: 'contentText' in msg ? msg.contentText : ('hydratedContentText' in msg ? msg.hydratedContentText : 'body' in msg ? msg.body?.text : '') || '' }
|
|
90
|
+
}
|
|
91
|
+
if (content?.buttonsMessage) return extractFromButtons(content.buttonsMessage)
|
|
92
|
+
if (content?.interactiveMessage) return extractFromButtons(content.interactiveMessage)
|
|
93
|
+
if (content?.templateMessage?.interactiveMessageTemplate) return extractFromButtons(content.templateMessage.interactiveMessageTemplate)
|
|
94
|
+
if (content?.templateMessage?.hydratedFourRowTemplate) return extractFromButtons(content.templateMessage.hydratedFourRowTemplate)
|
|
95
|
+
if (content?.templateMessage?.hydratedTemplate) return extractFromButtons(content.templateMessage.hydratedTemplate)
|
|
96
|
+
if (content?.templateMessage?.fourRowTemplate) return extractFromButtons(content.templateMessage.fourRowTemplate)
|
|
97
|
+
return content
|
|
98
|
+
}
|
|
56
99
|
|
|
57
|
-
//
|
|
100
|
+
// ─── MEDIA PREPARATION ────────────────────────────────────────────────────────
|
|
58
101
|
export const prepareWAMessageMedia = async (message, options) => {
|
|
59
|
-
|
|
102
|
+
const mediaType = MEDIA_KEYS.find(k => k in message)
|
|
60
103
|
if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
|
|
61
|
-
|
|
62
104
|
const uploadData = { ...message, media: message[mediaType] }
|
|
63
105
|
delete uploadData[mediaType]
|
|
64
|
-
|
|
65
|
-
const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache ? `${mediaType}:${uploadData.media.url.toString()}` : null
|
|
66
|
-
|
|
67
106
|
if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
|
|
68
107
|
if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
|
69
|
-
|
|
108
|
+
const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache
|
|
109
|
+
? `${mediaType}:${uploadData.media.url.toString()}` : null
|
|
70
110
|
if (cacheableKey) {
|
|
71
111
|
const cached = await options.mediaCache?.get(cacheableKey)
|
|
72
112
|
if (cached) {
|
|
73
|
-
|
|
113
|
+
options.logger?.debug({ cacheableKey }, 'got media cache hit')
|
|
114
|
+
const obj = WAProto.Message.decode(cached)
|
|
74
115
|
Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
|
|
75
116
|
return obj
|
|
76
117
|
}
|
|
77
118
|
}
|
|
78
|
-
|
|
119
|
+
const isNewsletter = !!options.jid && isJidNewsletter(options.jid)
|
|
79
120
|
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
|
80
121
|
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
|
|
81
122
|
const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor)
|
|
82
|
-
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
|
|
83
|
-
|
|
84
|
-
const encryptionResult = await (options.newsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
|
|
123
|
+
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation || requiresWaveformProcessing
|
|
124
|
+
const encryptionResult = await (isNewsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
|
|
85
125
|
logger: options.logger,
|
|
86
126
|
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
|
|
87
127
|
opts: options.options,
|
|
88
128
|
isPtt: uploadData.ptt,
|
|
89
|
-
forceOpus: mediaType === 'audio' && uploadData.mimetype
|
|
129
|
+
forceOpus: mediaType === 'audio' && uploadData.mimetype?.includes('opus'),
|
|
90
130
|
convertVideo: mediaType === 'video'
|
|
91
131
|
})
|
|
92
|
-
|
|
93
|
-
// ✅ FIX: Extract the correct values based on encryption method
|
|
94
132
|
const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
|
|
95
|
-
|
|
96
133
|
if (mediaType === 'audio' && opusConverted) uploadData.mimetype = 'audio/ogg; codecs=opus'
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
// ✅ FIX: Determine what to upload - use encFilePath for encrypted, encWriteStream for newsletter
|
|
101
|
-
const uploadStream = options.newsletter ? encWriteStream : (encFilePath || encWriteStream)
|
|
102
|
-
|
|
134
|
+
const fileEncSha256B64 = (isNewsletter ? fileSha256 : (fileEncSha256 ?? fileSha256)).toString('base64')
|
|
135
|
+
const uploadSource = isNewsletter ? encWriteStream : (encFilePath || encWriteStream)
|
|
103
136
|
const [{ mediaUrl, directPath, handle }] = await Promise.all([
|
|
104
137
|
(async () => {
|
|
105
|
-
const result = await options.upload(
|
|
138
|
+
const result = await options.upload(uploadSource, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
|
|
106
139
|
options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
|
|
107
140
|
return result
|
|
108
141
|
})(),
|
|
@@ -114,14 +147,20 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
114
147
|
if (!uploadData.width && originalImageDimensions) {
|
|
115
148
|
uploadData.width = originalImageDimensions.width
|
|
116
149
|
uploadData.height = originalImageDimensions.height
|
|
150
|
+
options.logger?.debug('set dimensions')
|
|
117
151
|
}
|
|
152
|
+
options.logger?.debug('generated thumbnail')
|
|
153
|
+
}
|
|
154
|
+
if (requiresDurationComputation) {
|
|
155
|
+
uploadData.seconds = await getAudioDuration(bodyPath)
|
|
156
|
+
options.logger?.debug('computed audio duration')
|
|
118
157
|
}
|
|
119
|
-
if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
|
|
120
158
|
if (requiresWaveformProcessing) {
|
|
121
159
|
try {
|
|
122
160
|
uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
|
|
123
|
-
|
|
124
|
-
|
|
161
|
+
options.logger?.debug('processed waveform')
|
|
162
|
+
} catch {
|
|
163
|
+
options.logger?.warn('failed to generate waveform, using fallback')
|
|
125
164
|
uploadData.waveform = new Uint8Array([0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99])
|
|
126
165
|
}
|
|
127
166
|
}
|
|
@@ -130,94 +169,88 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
130
169
|
})()
|
|
131
170
|
]).finally(async () => {
|
|
132
171
|
if (encWriteStream && !Buffer.isBuffer(encWriteStream)) encWriteStream.destroy?.()
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
try {
|
|
136
|
-
await fs.unlink(encFilePath)
|
|
137
|
-
} catch { }
|
|
138
|
-
}
|
|
139
|
-
if (didSaveToTmpPath && bodyPath) {
|
|
140
|
-
try {
|
|
141
|
-
await fs.access(bodyPath)
|
|
142
|
-
await fs.unlink(bodyPath)
|
|
143
|
-
} catch { }
|
|
144
|
-
}
|
|
172
|
+
if (encFilePath && typeof encFilePath === 'string') try { await fs.unlink(encFilePath) } catch { }
|
|
173
|
+
if (didSaveToTmpPath && bodyPath) try { await fs.access(bodyPath); await fs.unlink(bodyPath) } catch { }
|
|
145
174
|
})
|
|
146
|
-
|
|
147
175
|
const obj = WAProto.Message.fromObject({
|
|
148
176
|
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
|
|
149
|
-
url: handle ? undefined : mediaUrl,
|
|
150
|
-
|
|
177
|
+
url: handle ? undefined : mediaUrl,
|
|
178
|
+
directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
|
|
179
|
+
mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(),
|
|
180
|
+
...uploadData, media: undefined
|
|
151
181
|
})
|
|
152
182
|
})
|
|
153
|
-
|
|
154
183
|
if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
|
|
155
|
-
if (cacheableKey)
|
|
184
|
+
if (cacheableKey) {
|
|
185
|
+
options.logger?.debug({ cacheableKey }, 'set cache')
|
|
186
|
+
await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
|
|
187
|
+
}
|
|
156
188
|
return obj
|
|
157
189
|
}
|
|
158
190
|
|
|
159
191
|
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
|
|
160
192
|
ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
|
|
161
|
-
})
|
|
193
|
+
})
|
|
162
194
|
|
|
163
195
|
export const generateForwardMessageContent = (message, forceForward) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
196
|
+
let content = normalizeMessageContent(message.message)
|
|
197
|
+
if (!content) throw new Boom('no content in message', { statusCode: 400 })
|
|
198
|
+
content = proto.Message.decode(proto.Message.encode(content).finish())
|
|
199
|
+
let key = Object.keys(content)[0]
|
|
200
|
+
let score = (content?.[key]?.contextInfo?.forwardingScore || 0) + (message.key.fromMe && !forceForward ? 0 : 1)
|
|
168
201
|
if (key === 'conversation') {
|
|
169
|
-
content.extendedTextMessage = { text: content[key] }
|
|
170
|
-
delete content.conversation
|
|
171
|
-
key = 'extendedTextMessage'
|
|
202
|
+
content.extendedTextMessage = { text: content[key] }
|
|
203
|
+
delete content.conversation
|
|
204
|
+
key = 'extendedTextMessage'
|
|
172
205
|
}
|
|
206
|
+
content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {}
|
|
207
|
+
return content
|
|
208
|
+
}
|
|
173
209
|
|
|
174
|
-
|
|
175
|
-
return content;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// ===== MESSAGE HANDLERS =====
|
|
210
|
+
// ─── SUB-HANDLERS ─────────────────────────────────────────────────────────────
|
|
179
211
|
const handleTextMessage = async (message, options) => {
|
|
180
|
-
const extContent = { text: message.text }
|
|
181
|
-
let urlInfo = message.linkPreview
|
|
182
|
-
|
|
212
|
+
const extContent = { text: message.text }
|
|
213
|
+
let urlInfo = message.linkPreview
|
|
214
|
+
if (typeof urlInfo === 'undefined') urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger)
|
|
183
215
|
if (urlInfo) {
|
|
184
216
|
Object.assign(extContent, {
|
|
185
217
|
matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
|
|
186
218
|
description: urlInfo.description, title: urlInfo.title, previewType: 0
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
194
|
-
}
|
|
219
|
+
})
|
|
220
|
+
const img = urlInfo.highQualityThumbnail
|
|
221
|
+
if (img) Object.assign(extContent, {
|
|
222
|
+
thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
|
|
223
|
+
thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
|
|
224
|
+
})
|
|
195
225
|
}
|
|
196
|
-
|
|
197
|
-
if (options.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
226
|
+
if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor)
|
|
227
|
+
if (options.font) extContent.font = options.font
|
|
228
|
+
return { extendedTextMessage: extContent }
|
|
229
|
+
}
|
|
201
230
|
|
|
202
231
|
const handleSpecialMessages = async (message, options) => {
|
|
203
232
|
if ('contacts' in message) {
|
|
204
|
-
const { contacts } = message.contacts
|
|
205
|
-
if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 })
|
|
206
|
-
return contacts.length === 1
|
|
233
|
+
const { contacts } = message.contacts
|
|
234
|
+
if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 })
|
|
235
|
+
return contacts.length === 1
|
|
236
|
+
? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) }
|
|
237
|
+
: { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) }
|
|
207
238
|
}
|
|
208
|
-
if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) }
|
|
239
|
+
if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) }
|
|
209
240
|
if ('react' in message) {
|
|
210
|
-
if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now()
|
|
211
|
-
return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) }
|
|
241
|
+
if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now()
|
|
242
|
+
return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) }
|
|
212
243
|
}
|
|
213
|
-
if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } }
|
|
214
|
-
if ('forward' in message) return generateForwardMessageContent(message.forward, message.force)
|
|
244
|
+
if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } }
|
|
245
|
+
if ('forward' in message) return generateForwardMessageContent(message.forward, message.force)
|
|
215
246
|
if ('disappearingMessagesInChat' in message) {
|
|
216
|
-
const exp = typeof message.disappearingMessagesInChat === 'boolean'
|
|
217
|
-
|
|
247
|
+
const exp = typeof message.disappearingMessagesInChat === 'boolean'
|
|
248
|
+
? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
|
|
249
|
+
: message.disappearingMessagesInChat
|
|
250
|
+
return prepareDisappearingMessageSettingContent(exp)
|
|
218
251
|
}
|
|
219
|
-
return null
|
|
220
|
-
}
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
221
254
|
|
|
222
255
|
const handleGroupInvite = async (message, options) => {
|
|
223
256
|
const m = {
|
|
@@ -225,20 +258,19 @@ const handleGroupInvite = async (message, options) => {
|
|
|
225
258
|
inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
|
|
226
259
|
caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
|
|
227
260
|
}
|
|
228
|
-
}
|
|
229
|
-
|
|
261
|
+
}
|
|
230
262
|
if (options.getProfilePicUrl) {
|
|
231
|
-
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview')
|
|
263
|
+
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview')
|
|
232
264
|
if (pfpUrl) {
|
|
233
|
-
const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher })
|
|
234
|
-
if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer())
|
|
265
|
+
const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher })
|
|
266
|
+
if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer())
|
|
235
267
|
}
|
|
236
268
|
}
|
|
237
|
-
return m
|
|
238
|
-
}
|
|
269
|
+
return m
|
|
270
|
+
}
|
|
239
271
|
|
|
240
|
-
const handleEventMessage = (message, options) => {
|
|
241
|
-
const startTime = Math.floor(message.event.startDate.getTime() / 1000)
|
|
272
|
+
const handleEventMessage = async (message, options) => {
|
|
273
|
+
const startTime = Math.floor(message.event.startDate.getTime() / 1000)
|
|
242
274
|
const m = {
|
|
243
275
|
eventMessage: {
|
|
244
276
|
name: message.event.name, description: message.event.description, startTime,
|
|
@@ -247,460 +279,543 @@ const handleEventMessage = (message, options) => {
|
|
|
247
279
|
isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
|
|
248
280
|
},
|
|
249
281
|
messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
|
|
250
|
-
}
|
|
251
|
-
|
|
282
|
+
}
|
|
252
283
|
if (message.event.call && options.getCallLink) {
|
|
253
|
-
options.getCallLink(message.event.call, { startTime })
|
|
254
|
-
|
|
255
|
-
});
|
|
284
|
+
const token = await options.getCallLink(message.event.call, { startTime })
|
|
285
|
+
m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token
|
|
256
286
|
}
|
|
257
|
-
return m
|
|
258
|
-
}
|
|
287
|
+
return m
|
|
288
|
+
}
|
|
259
289
|
|
|
260
290
|
const handlePollMessage = (message) => {
|
|
261
|
-
message.poll.selectableCount ||= 0
|
|
262
|
-
message.poll.toAnnouncementGroup ||= false
|
|
263
|
-
|
|
264
|
-
if (!Array.isArray(message.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 });
|
|
291
|
+
message.poll.selectableCount ||= 0
|
|
292
|
+
message.poll.toAnnouncementGroup ||= false
|
|
293
|
+
if (!Array.isArray(message.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 })
|
|
265
294
|
if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length)
|
|
266
|
-
throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 })
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
295
|
+
throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 })
|
|
296
|
+
const pollMsg = {
|
|
297
|
+
name: message.poll.name,
|
|
298
|
+
selectableOptionsCount: message.poll.selectableCount,
|
|
299
|
+
options: message.poll.values.map(optionName => ({ optionName }))
|
|
300
|
+
}
|
|
301
|
+
const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } }
|
|
302
|
+
if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg
|
|
303
|
+
else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg
|
|
304
|
+
else m.pollCreationMessage = pollMsg
|
|
305
|
+
return m
|
|
306
|
+
}
|
|
275
307
|
|
|
276
308
|
const handleProductMessage = async (message, options) => {
|
|
277
|
-
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options)
|
|
278
|
-
return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) }
|
|
279
|
-
}
|
|
309
|
+
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options)
|
|
310
|
+
return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) }
|
|
311
|
+
}
|
|
280
312
|
|
|
281
313
|
const handleRequestPayment = async (message, options) => {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
314
|
+
const data = message.requestPayment || message.payment
|
|
315
|
+
const sticker = data.sticker ? await prepareWAMessageMedia({ sticker: data.sticker }, options) : null
|
|
316
|
+
let notes
|
|
317
|
+
if (sticker) {
|
|
318
|
+
notes = { stickerMessage: { ...sticker.stickerMessage, contextInfo: data.contextInfo } }
|
|
319
|
+
} else if (data.note) {
|
|
320
|
+
notes = { extendedTextMessage: { text: data.note, contextInfo: data.contextInfo } }
|
|
321
|
+
} else {
|
|
322
|
+
notes = { extendedTextMessage: { text: data.note || 'Notes' } }
|
|
323
|
+
}
|
|
289
324
|
const m = {
|
|
290
325
|
requestPaymentMessage: WAProto.Message.RequestPaymentMessage.fromObject({
|
|
291
|
-
expiryTimestamp:
|
|
292
|
-
amount1000:
|
|
293
|
-
currencyCodeIso4217:
|
|
294
|
-
requestFrom:
|
|
295
|
-
noteMessage: notes,
|
|
326
|
+
expiryTimestamp: data.expiryTimestamp || data.expiry || 0,
|
|
327
|
+
amount1000: data.amount1000 || data.amount || 0,
|
|
328
|
+
currencyCodeIso4217: data.currencyCodeIso4217 || data.currency || 'IDR',
|
|
329
|
+
requestFrom: data.requestFrom || data.from || '0@s.whatsapp.net',
|
|
330
|
+
noteMessage: notes,
|
|
331
|
+
background: data.background ?? { id: 'DEFAULT', placeholderArgb: 0xfff0f0f0 }
|
|
296
332
|
})
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (
|
|
333
|
+
}
|
|
334
|
+
// BRL Pix key support
|
|
335
|
+
if ((data.currencyCodeIso4217 === 'BRL' || data.currency === 'BRL') && data.pixKey) {
|
|
300
336
|
if (!m.requestPaymentMessage.noteMessage.extendedTextMessage)
|
|
301
|
-
m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } }
|
|
302
|
-
m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${
|
|
337
|
+
m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } }
|
|
338
|
+
m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${data.pixKey}`
|
|
303
339
|
}
|
|
340
|
+
return m
|
|
341
|
+
}
|
|
304
342
|
|
|
305
|
-
|
|
306
|
-
|
|
343
|
+
const handleButtonReply = (message) => {
|
|
344
|
+
switch (message.type) {
|
|
345
|
+
case 'list': return { listResponseMessage: { title: message.buttonReply.title, description: message.buttonReply.description, singleSelectReply: { selectedRowId: message.buttonReply.rowId }, lisType: proto.Message.ListResponseMessage.ListType.SINGLE_SELECT } }
|
|
346
|
+
case 'template': return { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } }
|
|
347
|
+
case 'interactive': return { interactiveResponseMessage: { body: { text: message.buttonReply.displayText, format: proto.Message.InteractiveResponseMessage.Body.Format.EXTENSIONS_1 }, nativeFlowResponseMessage: { name: message.buttonReply.nativeFlows?.name, paramsJson: message.buttonReply.nativeFlows?.paramsJson, version: message.buttonReply.nativeFlows?.version } } }
|
|
348
|
+
default: return { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT } }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
307
351
|
|
|
308
|
-
//
|
|
352
|
+
// ─── MAIN GENERATOR ───────────────────────────────────────────────────────────
|
|
309
353
|
export const generateWAMessageContent = async (message, options = {}) => {
|
|
310
|
-
const messageKeys = Object.keys(message)
|
|
311
|
-
|
|
312
|
-
// ===== SMART DETECTION =====
|
|
313
|
-
const isRawProtoMessage = messageKeys.some(key =>
|
|
314
|
-
key.endsWith('Message') &&
|
|
315
|
-
typeof message[key] === 'object' &&
|
|
316
|
-
!HIGH_LEVEL_KEYS.includes(key)
|
|
317
|
-
);
|
|
354
|
+
const messageKeys = Object.keys(message)
|
|
318
355
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1)
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// If no high-level keys AND has proto message keys, pass through
|
|
327
|
-
if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) {
|
|
328
|
-
return WAProto.Message.create(message);
|
|
329
|
-
}
|
|
356
|
+
// ─── PROTO PASSTHROUGH ────────────────────────────────────────────────────
|
|
357
|
+
const isRawProtoMessage = messageKeys.some(k => k.endsWith('Message') && typeof message[k] === 'object' && !HIGH_LEVEL_KEYS.includes(k))
|
|
358
|
+
const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message)
|
|
359
|
+
if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) return WAProto.Message.create(message)
|
|
360
|
+
if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) return WAProto.Message.create(message)
|
|
330
361
|
|
|
331
|
-
let m = {}
|
|
362
|
+
let m = {}
|
|
332
363
|
|
|
333
|
-
//
|
|
364
|
+
// ─── TEXT ─────────────────────────────────────────────────────────────────
|
|
334
365
|
if ('text' in message && !('buttons' in message) && !('templateButtons' in message) && !('sections' in message) && !('interactiveButtons' in message) && !('shop' in message)) {
|
|
335
|
-
m = await handleTextMessage(message, options)
|
|
366
|
+
m = await handleTextMessage(message, options)
|
|
336
367
|
}
|
|
337
368
|
|
|
338
|
-
//
|
|
369
|
+
// ─── SPECIAL / MEDIA ──────────────────────────────────────────────────────
|
|
339
370
|
else {
|
|
340
|
-
const special = await handleSpecialMessages(message, options)
|
|
341
|
-
if (special)
|
|
342
|
-
|
|
343
|
-
else if ('
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
else if ('
|
|
360
|
-
|
|
361
|
-
else if ('
|
|
362
|
-
|
|
363
|
-
else if ('
|
|
364
|
-
|
|
365
|
-
else if ('
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
else if ('
|
|
369
|
-
|
|
370
|
-
else if ('
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
const special = await handleSpecialMessages(message, options)
|
|
372
|
+
if (special) {
|
|
373
|
+
m = special
|
|
374
|
+
} else if ('groupInvite' in message) {
|
|
375
|
+
m = await handleGroupInvite(message, options)
|
|
376
|
+
} else if ('stickerPack' in message) {
|
|
377
|
+
return WAProto.Message.create({ stickerPackMessage: (await prepareStickerPackMessage(message.stickerPack, options)).stickerPackMessage })
|
|
378
|
+
} else if ('pin' in message) {
|
|
379
|
+
const messageKey = typeof message.pin === 'boolean'
|
|
380
|
+
? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation') })())
|
|
381
|
+
: typeof message.pin === 'object'
|
|
382
|
+
? (message.pin.key || (message.pin.id ? { remoteJid: options.jid, fromMe: message.pin.fromMe || false, id: message.pin.id, participant: message.pin.participant } : null))
|
|
383
|
+
: message.pin
|
|
384
|
+
const shouldPin = typeof message.pin === 'boolean' ? message.pin : (message.pin?.unpin !== true)
|
|
385
|
+
const pinTime = typeof message.pin === 'object' ? message.pin.time : message.time
|
|
386
|
+
if (!messageKey?.id) throw new Boom('Invalid message key for pin operation')
|
|
387
|
+
m = { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (pinTime || 86400) : 0 } }
|
|
388
|
+
} else if ('keep' in message) {
|
|
389
|
+
m = { keepInChatMessage: { key: message.keep, keepType: message.type, timestampMs: Date.now() } }
|
|
390
|
+
} else if ('call' in message) {
|
|
391
|
+
m = { scheduledCallCreationMessage: { scheduledTimestampMs: message.call.time || Date.now(), callType: message.call.type || 1, title: message.call.title } }
|
|
392
|
+
} else if ('paymentInvite' in message) {
|
|
393
|
+
m = { paymentInviteMessage: { serviceType: message.paymentInvite.type, expiryTimestamp: message.paymentInvite.expiry } }
|
|
394
|
+
} else if ('buttonReply' in message) {
|
|
395
|
+
m = handleButtonReply(message)
|
|
396
|
+
} else if ('ptv' in message && message.ptv) {
|
|
397
|
+
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options)
|
|
398
|
+
m = { ptvMessage: videoMessage }
|
|
399
|
+
} else if ('product' in message) {
|
|
400
|
+
m = await handleProductMessage(message, options)
|
|
401
|
+
} else if ('order' in message) {
|
|
402
|
+
m = { orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: message.order.id, thumbnail: message.order.thumbnail, itemCount: message.order.itemCount, status: message.order.status, surface: message.order.surface, orderTitle: message.order.title, message: message.order.text, sellerJid: message.order.seller, token: message.order.token, totalAmount1000: message.order.amount, totalCurrencyCode: message.order.currency }) }
|
|
403
|
+
} else if ('sections' in message && message.sections) {
|
|
404
|
+
m = {
|
|
405
|
+
listMessage: {
|
|
406
|
+
title: message.title, buttonText: message.buttonText, footerText: message.footer,
|
|
407
|
+
description: message.text, sections: message.sections,
|
|
408
|
+
listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
|
|
409
|
+
contextInfo: { ...(message.contextInfo || {}), ...(message.mentions ? { mentionedJid: message.mentions } : {}) }
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} else if ('listReply' in message) {
|
|
413
|
+
m = { listResponseMessage: { ...message.listReply } }
|
|
414
|
+
} else if ('event' in message) {
|
|
415
|
+
m = await handleEventMessage(message, options)
|
|
416
|
+
} else if ('poll' in message) {
|
|
417
|
+
m = handlePollMessage(message)
|
|
418
|
+
} else if ('inviteAdmin' in message) {
|
|
419
|
+
m = { newsletterAdminInviteMessage: { inviteExpiration: message.inviteAdmin.inviteExpiration, caption: message.inviteAdmin.text, newsletterJid: message.inviteAdmin.jid, newsletterName: message.inviteAdmin.subject, jpegThumbnail: message.inviteAdmin.thumbnail } }
|
|
420
|
+
} else if ('requestPayment' in message || 'payment' in message) {
|
|
421
|
+
m = await handleRequestPayment(message, options)
|
|
422
|
+
} else if ('extendedTextMessage' in message) {
|
|
423
|
+
m = { extendedTextMessage: WAProto.Message.ExtendedTextMessage.create(message.extendedTextMessage) }
|
|
424
|
+
} else if ('interactiveMessage' in message) {
|
|
425
|
+
m = { interactiveMessage: WAProto.Message.InteractiveMessage.create(message.interactiveMessage) }
|
|
426
|
+
} else if ('sharePhoneNumber' in message) {
|
|
427
|
+
m = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER } }
|
|
428
|
+
} else if ('requestPhoneNumber' in message) {
|
|
429
|
+
m = { requestPhoneNumberMessage: {} }
|
|
430
|
+
} else if ('limitSharing' in message) {
|
|
431
|
+
m = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING, limitSharing: { sharingLimited: message.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } }
|
|
432
|
+
} else if ('album' in message) {
|
|
433
|
+
const imageItems = message.album.filter(i => 'image' in i)
|
|
434
|
+
const videoItems = message.album.filter(i => 'video' in i)
|
|
435
|
+
m = { albumMessage: { expectedImageCount: imageItems.length, expectedVideoCount: videoItems.length } }
|
|
436
|
+
} else if (MEDIA_KEYS.some(k => k in message)) {
|
|
437
|
+
m = await prepareWAMessageMedia(message, options)
|
|
375
438
|
}
|
|
376
|
-
else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
|
|
377
439
|
}
|
|
378
440
|
|
|
379
|
-
//
|
|
441
|
+
// ─── SMART BUTTON HANDLING ────────────────────────────────────────────────
|
|
380
442
|
if ('buttons' in message && Array.isArray(message.buttons) && message.buttons.length > 0) {
|
|
381
|
-
const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson)
|
|
382
|
-
|
|
443
|
+
const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson)
|
|
383
444
|
if (hasNativeFlow) {
|
|
384
|
-
// Convert to interactiveMessage
|
|
385
445
|
const interactive = {
|
|
386
446
|
body: { text: message.text || message.caption || message.contentText || '' },
|
|
387
447
|
footer: { text: message.footer || message.footerText || '' },
|
|
388
448
|
nativeFlowMessage: {
|
|
389
449
|
buttons: message.buttons.map(btn => {
|
|
390
|
-
if (btn.name && btn.buttonParamsJson) return btn
|
|
391
|
-
if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson }
|
|
392
|
-
return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) }
|
|
450
|
+
if (btn.name && btn.buttonParamsJson) return btn
|
|
451
|
+
if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson }
|
|
452
|
+
return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) }
|
|
393
453
|
})
|
|
394
454
|
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
|
|
455
|
+
}
|
|
456
|
+
if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle || '', hasMediaAttachment: message.hasMediaAttachment || false }
|
|
398
457
|
if (Object.keys(m).length > 0) {
|
|
399
|
-
interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true }
|
|
400
|
-
Object.assign(interactive.header, m)
|
|
458
|
+
interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true }
|
|
459
|
+
Object.assign(interactive.header, m)
|
|
401
460
|
}
|
|
402
|
-
|
|
403
|
-
m = { interactiveMessage: interactive };
|
|
461
|
+
m = { interactiveMessage: interactive }
|
|
404
462
|
} else {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
463
|
+
const buttonsMessage = { buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) }
|
|
464
|
+
if ('text' in message) { buttonsMessage.contentText = message.text; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY }
|
|
465
|
+
else {
|
|
466
|
+
if ('caption' in message) buttonsMessage.contentText = message.caption
|
|
467
|
+
const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase()
|
|
468
|
+
buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY
|
|
469
|
+
Object.assign(buttonsMessage, m)
|
|
470
|
+
}
|
|
471
|
+
if (message.title) { buttonsMessage.text = message.title; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT }
|
|
472
|
+
if (message.footer) buttonsMessage.footerText = message.footer
|
|
473
|
+
m = { buttonsMessage }
|
|
412
474
|
}
|
|
413
475
|
}
|
|
414
476
|
|
|
415
|
-
//
|
|
416
|
-
else if ('templateButtons' in message &&
|
|
417
|
-
const
|
|
418
|
-
if ('text' in message)
|
|
419
|
-
else { if ('caption' in message)
|
|
420
|
-
if (
|
|
421
|
-
m = { templateMessage: { fourRowTemplate:
|
|
477
|
+
// ─── TEMPLATE BUTTONS ─────────────────────────────────────────────────────
|
|
478
|
+
else if ('templateButtons' in message && message.templateButtons) {
|
|
479
|
+
const hydratedTemplate = { hydratedButtons: message.templateButtons }
|
|
480
|
+
if ('text' in message) hydratedTemplate.hydratedContentText = message.text
|
|
481
|
+
else { if ('caption' in message) hydratedTemplate.hydratedContentText = message.caption; Object.assign(hydratedTemplate, m) }
|
|
482
|
+
if (message.footer) hydratedTemplate.hydratedFooterText = message.footer
|
|
483
|
+
m = { templateMessage: { fourRowTemplate: hydratedTemplate, hydratedTemplate } }
|
|
422
484
|
}
|
|
423
485
|
|
|
424
|
-
//
|
|
425
|
-
else if ('
|
|
426
|
-
|
|
486
|
+
// ─── INTERACTIVE BUTTONS ──────────────────────────────────────────────────
|
|
487
|
+
else if ('interactiveButtons' in message && message.interactiveButtons) {
|
|
488
|
+
const interactive = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: message.interactiveButtons }) }
|
|
489
|
+
if ('text' in message) { interactive.body = { text: message.text }; interactive.header = { title: message.title || '', subtitle: message.subtitle || '', hasMediaAttachment: false } }
|
|
490
|
+
else if ('caption' in message) {
|
|
491
|
+
interactive.body = { text: message.caption }
|
|
492
|
+
interactive.header = { title: message.title || '', subtitle: message.subtitle || '', hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0) }
|
|
493
|
+
if (Object.keys(m).length > 0) Object.assign(interactive.header, m)
|
|
494
|
+
}
|
|
495
|
+
if (message.footer) interactive.footer = { text: message.footer }
|
|
496
|
+
m = { interactiveMessage: interactive, messageContextInfo: { messageSecret: randomBytes(32) } }
|
|
427
497
|
}
|
|
428
498
|
|
|
429
|
-
//
|
|
430
|
-
else if ('
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if ('
|
|
435
|
-
if ('
|
|
436
|
-
m
|
|
499
|
+
// ─── SHOP MESSAGE ─────────────────────────────────────────────────────────
|
|
500
|
+
else if ('shop' in message && message.shop) {
|
|
501
|
+
const interactive = {
|
|
502
|
+
shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({ surface: message.shop.surface || 1, id: message.shop.id || message.id })
|
|
503
|
+
}
|
|
504
|
+
if ('text' in message) interactive.body = { text: message.text }
|
|
505
|
+
else if ('caption' in message) interactive.body = { text: message.caption }
|
|
506
|
+
if (message.title || Object.keys(m).length > 0) {
|
|
507
|
+
interactive.header = { title: message.title || '', subtitle: message.subtitle || '', hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0) }
|
|
508
|
+
if (Object.keys(m).length > 0) Object.assign(interactive.header, m)
|
|
509
|
+
}
|
|
510
|
+
if (message.footer) interactive.footer = { text: message.footer }
|
|
511
|
+
m = { interactiveMessage: interactive }
|
|
437
512
|
}
|
|
438
513
|
|
|
439
|
-
//
|
|
440
|
-
else if ('
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
// Handle body text
|
|
449
|
-
if ('text' in message) interactiveMessage.body = { text: message.text };
|
|
450
|
-
else if ('caption' in message) interactiveMessage.body = { text: message.caption };
|
|
451
|
-
|
|
452
|
-
// Handle header with media
|
|
453
|
-
if (message.title || message.subtitle || Object.keys(m).length > 0) {
|
|
454
|
-
interactiveMessage.header = {
|
|
455
|
-
title: message.title || '',
|
|
456
|
-
subtitle: message.subtitle || '',
|
|
457
|
-
hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0)
|
|
458
|
-
};
|
|
459
|
-
if (Object.keys(m).length > 0) Object.assign(interactiveMessage.header, m);
|
|
514
|
+
// ─── COLLECTION ───────────────────────────────────────────────────────────
|
|
515
|
+
else if ('collection' in message && message.collection) {
|
|
516
|
+
const interactive = { collectionMessage: { bizJid: message.collection.bizJid, id: message.collection.id, messageVersion: message.collection.version } }
|
|
517
|
+
if ('text' in message) { interactive.body = { text: message.text }; interactive.header = { title: message.title || '', hasMediaAttachment: false } }
|
|
518
|
+
else if ('caption' in message) {
|
|
519
|
+
interactive.body = { text: message.caption }
|
|
520
|
+
interactive.header = { title: message.title || '', hasMediaAttachment: message.hasMediaAttachment ?? false }
|
|
521
|
+
if (Object.keys(m).length > 0) Object.assign(interactive.header, m)
|
|
460
522
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
m = { interactiveMessage };
|
|
523
|
+
if (message.footer) interactive.footer = { text: message.footer }
|
|
524
|
+
m = { interactiveMessage: interactive }
|
|
465
525
|
}
|
|
466
526
|
|
|
467
|
-
//
|
|
468
|
-
const finalKey = Object.keys(m)[0]
|
|
469
|
-
|
|
470
|
-
// Auto-merge contextInfo and mentions
|
|
471
|
-
if ((message.contextInfo || message.mentions) && finalKey && m[finalKey]) {
|
|
527
|
+
// ─── AUTO CONTEXT + MENTIONS MERGE ────────────────────────────────────────
|
|
528
|
+
const finalKey = Object.keys(m)[0]
|
|
529
|
+
if ((message.contextInfo || message.mentions?.length) && finalKey && m[finalKey] && typeof m[finalKey] === 'object') {
|
|
472
530
|
m[finalKey].contextInfo = {
|
|
473
531
|
...(m[finalKey].contextInfo || {}),
|
|
474
532
|
...(message.contextInfo || {}),
|
|
475
|
-
mentionedJid: message.mentions
|
|
476
|
-
}
|
|
533
|
+
...(message.mentions?.length ? { mentionedJid: message.mentions } : {})
|
|
534
|
+
}
|
|
477
535
|
}
|
|
478
536
|
|
|
479
|
-
//
|
|
480
|
-
if (
|
|
537
|
+
// ─── WRAPPERS ─────────────────────────────────────────────────────────────
|
|
538
|
+
if (('viewOnce' in message && message.viewOnce) || ('viewOnceMessage' in message && message.viewOnceMessage)) {
|
|
539
|
+
m = { viewOnceMessage: { message: m } }
|
|
540
|
+
}
|
|
541
|
+
if ('edit' in message) {
|
|
542
|
+
m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } }
|
|
543
|
+
}
|
|
544
|
+
if ('contextInfo' in message && message.contextInfo) {
|
|
545
|
+
const k = Object.keys(m)[0]
|
|
546
|
+
if (k && m[k]) m[k].contextInfo = { ...(m[k].contextInfo || {}), ...message.contextInfo }
|
|
547
|
+
}
|
|
481
548
|
|
|
482
|
-
|
|
483
|
-
|
|
549
|
+
if (shouldIncludeReportingToken(m)) {
|
|
550
|
+
m.messageContextInfo = m.messageContextInfo || {}
|
|
551
|
+
if (!m.messageContextInfo.messageSecret) {
|
|
552
|
+
m.messageContextInfo.messageSecret = randomBytes(32)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
484
555
|
|
|
485
|
-
return WAProto.Message.create(m)
|
|
486
|
-
}
|
|
556
|
+
return WAProto.Message.create(m)
|
|
557
|
+
}
|
|
487
558
|
|
|
559
|
+
// ─── STICKER PACK ─────────────────────────────────────────────────────────────
|
|
560
|
+
export const prepareStickerPackMessage = async (stickerPack, options) => {
|
|
561
|
+
const { stickers, cover, name, publisher, packId, description } = stickerPack
|
|
562
|
+
if (!stickers?.length) throw new Boom('Sticker pack requires at least one sticker', { statusCode: 400 })
|
|
563
|
+
if (stickers.length > 120) throw new Boom('Sticker pack exceeds maximum of 120 stickers', { statusCode: 400 })
|
|
564
|
+
const lib = await getImageProcessingLibrary()
|
|
565
|
+
const packId_ = packId || generateMessageIDV2()
|
|
566
|
+
const isWebPBuffer = (buf) => buf.length >= 12 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50
|
|
567
|
+
const isAnimatedWebP = (buf) => {
|
|
568
|
+
if (!isWebPBuffer(buf)) return false
|
|
569
|
+
let offset = 12
|
|
570
|
+
while (offset < buf.length - 8) {
|
|
571
|
+
const fourCC = buf.toString('ascii', offset, offset + 4)
|
|
572
|
+
const chunkSize = buf.readUInt32LE(offset + 4)
|
|
573
|
+
if (fourCC === 'VP8X' && offset + 8 < buf.length && (buf[offset + 8] & 0x02)) return true
|
|
574
|
+
if (fourCC === 'ANIM' || fourCC === 'ANMF') return true
|
|
575
|
+
offset += 8 + chunkSize + (chunkSize % 2)
|
|
576
|
+
}
|
|
577
|
+
return false
|
|
578
|
+
}
|
|
579
|
+
const toWebp = async (buffer) => {
|
|
580
|
+
if (isWebPBuffer(buffer)) {
|
|
581
|
+
if ('sharp' in lib && lib.sharp) return await lib.sharp.default(buffer).resize(512, 512, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75, effort: 6 }).toBuffer()
|
|
582
|
+
return buffer
|
|
583
|
+
}
|
|
584
|
+
if ('sharp' in lib && lib.sharp) return await lib.sharp.default(buffer).resize(512, 512, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75, effort: 6 }).toBuffer()
|
|
585
|
+
if ('jimp' in lib && lib.jimp) return await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'))
|
|
586
|
+
throw new Boom('No image processing library available for WebP conversion', { statusCode: 500 })
|
|
587
|
+
}
|
|
588
|
+
const validStickers = []
|
|
589
|
+
await Promise.all(stickers.map(async (s) => {
|
|
590
|
+
try {
|
|
591
|
+
const { stream } = await getStream(s.data || s.sticker)
|
|
592
|
+
const buffer = await toBuffer(stream)
|
|
593
|
+
if (!buffer?.length) return
|
|
594
|
+
const animated = isAnimatedWebP(buffer)
|
|
595
|
+
let webpBuffer = await toWebp(buffer)
|
|
596
|
+
if (webpBuffer.length > 1024 * 1024) {
|
|
597
|
+
if ('sharp' in lib && lib.sharp) webpBuffer = await lib.sharp.default(webpBuffer).webp({ quality: 50 }).toBuffer()
|
|
598
|
+
if (webpBuffer.length > 1024 * 1024) return
|
|
599
|
+
}
|
|
600
|
+
const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-').replace(/=/g, '')
|
|
601
|
+
validStickers.push({
|
|
602
|
+
fileName: `${hash}.webp`, buffer: webpBuffer, mimetype: 'image/webp',
|
|
603
|
+
isAnimated: s.isAnimated ?? animated, isLottie: s.isLottie || false,
|
|
604
|
+
emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel || ''
|
|
605
|
+
})
|
|
606
|
+
} catch (e) { options.logger?.warn({ err: e }, 'failed processing sticker') }
|
|
607
|
+
}))
|
|
608
|
+
if (!validStickers.length) throw new Boom('No valid stickers could be processed', { statusCode: 400 })
|
|
609
|
+
const { stream: covStream } = await getStream(cover)
|
|
610
|
+
const coverBuffer = await toWebp(await toBuffer(covStream))
|
|
611
|
+
const processBatch = async (batch, batchIdx) => {
|
|
612
|
+
const batchData = {}
|
|
613
|
+
batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 6 }] })
|
|
614
|
+
const trayFile = `${packId_}_${batchIdx}.webp`
|
|
615
|
+
batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 6 }]
|
|
616
|
+
const zipBuf = await new Promise((resolve, reject) => zip(batchData, { level: 6, memLevel: 9 }, (err, data) => err ? reject(err) : resolve(Buffer.from(data))))
|
|
617
|
+
if (zipBuf.length > 10 * 1024 * 1024) throw new Boom(`Sticker pack batch ${batchIdx} too large: ${(zipBuf.length / 1024 / 1024).toFixed(2)}MB`, { statusCode: 400 })
|
|
618
|
+
const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options })
|
|
619
|
+
const uploadRes = await options.upload(upload.encFilePath, { fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs || 300000 })
|
|
620
|
+
try { await fs.unlink(upload.encFilePath) } catch { }
|
|
621
|
+
let thumbRes = null
|
|
622
|
+
try {
|
|
623
|
+
let thumbBuf
|
|
624
|
+
if ('sharp' in lib && lib.sharp) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252, { fit: 'cover' }).jpeg({ quality: 80 }).toBuffer()
|
|
625
|
+
else if ('jimp' in lib && lib.jimp) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'))
|
|
626
|
+
if (thumbBuf?.length) {
|
|
627
|
+
const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey })
|
|
628
|
+
thumbRes = await options.upload(thumbUpload.encFilePath, { fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs || 60000 })
|
|
629
|
+
try { await fs.unlink(thumbUpload.encFilePath) } catch { }
|
|
630
|
+
thumbRes._buf = thumbBuf
|
|
631
|
+
thumbRes._enc = thumbUpload
|
|
632
|
+
}
|
|
633
|
+
} catch (e) { options.logger?.warn({ err: e }, 'failed generating sticker pack thumbnail') }
|
|
634
|
+
return {
|
|
635
|
+
name: batchIdx > 0 ? `${name} (${batchIdx + 1})` : name, publisher, packDescription: description,
|
|
636
|
+
stickerPackId: batchIdx > 0 ? `${packId_}_${batchIdx}` : packId_,
|
|
637
|
+
stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.THIRD_PARTY,
|
|
638
|
+
stickerPackSize: zipBuf.length,
|
|
639
|
+
stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
|
|
640
|
+
fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
|
|
641
|
+
directPath: uploadRes.directPath, fileLength: upload.fileLength,
|
|
642
|
+
mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
|
|
643
|
+
...(thumbRes && {
|
|
644
|
+
thumbnailDirectPath: thumbRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252,
|
|
645
|
+
thumbnailSha256: thumbRes._enc?.fileSha256, thumbnailEncSha256: thumbRes._enc?.fileEncSha256,
|
|
646
|
+
imageDataHash: thumbRes._buf ? sha256(thumbRes._buf).toString('base64') : undefined
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (validStickers.length > 60) {
|
|
651
|
+
const batches = []
|
|
652
|
+
for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60))
|
|
653
|
+
const results = await Promise.all(batches.map((b, i) => processBatch(b, i)))
|
|
654
|
+
return { stickerPackMessage: results, isBatched: true, batchCount: batches.length }
|
|
655
|
+
}
|
|
656
|
+
return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false }
|
|
657
|
+
}
|
|
488
658
|
|
|
659
|
+
// ─── MESSAGE BUILDERS ─────────────────────────────────────────────────────────
|
|
489
660
|
export const generateWAMessageFromContent = (jid, message, options) => {
|
|
490
|
-
if (!options.timestamp) options.timestamp = new Date()
|
|
491
|
-
const innerMessage = normalizeMessageContent(message)
|
|
492
|
-
const key = getContentType(innerMessage)
|
|
493
|
-
const
|
|
494
|
-
const { quoted, userJid } = options;
|
|
495
|
-
|
|
661
|
+
if (!options.timestamp) options.timestamp = new Date()
|
|
662
|
+
const innerMessage = normalizeMessageContent(message)
|
|
663
|
+
const key = getContentType(innerMessage)
|
|
664
|
+
const { quoted, userJid } = options
|
|
496
665
|
if (quoted && !isJidNewsletter(jid)) {
|
|
497
|
-
const participant = quoted.key.fromMe ? userJid : quoted.participant || quoted.key.participant || quoted.key.remoteJid
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
contextInfo
|
|
503
|
-
|
|
504
|
-
|
|
666
|
+
const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
|
|
667
|
+
const normalizedQuoted = normalizeMessageContent(quoted.message)
|
|
668
|
+
const quotedType = getContentType(normalizedQuoted)
|
|
669
|
+
const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
|
|
670
|
+
const quotedContent = quotedMsg[quotedType]
|
|
671
|
+
if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
|
|
672
|
+
const contextInfo = (innerMessage[key]?.contextInfo) || {}
|
|
673
|
+
contextInfo.participant = jidNormalizedUser(participant)
|
|
674
|
+
contextInfo.stanzaId = quoted.key.id
|
|
675
|
+
contextInfo.quotedMessage = quotedMsg
|
|
676
|
+
if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
|
|
677
|
+
if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
|
|
505
678
|
}
|
|
506
|
-
|
|
507
679
|
if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
|
|
508
|
-
innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
|
|
680
|
+
innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
|
|
509
681
|
}
|
|
510
|
-
|
|
511
682
|
return WAProto.WebMessageInfo.fromObject({
|
|
512
|
-
key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2()
|
|
513
|
-
message: WAProto.Message.
|
|
514
|
-
messageTimestamp: timestamp,
|
|
683
|
+
key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
|
|
684
|
+
message: WAProto.Message.fromObject(message),
|
|
685
|
+
messageTimestamp: unixTimestampSeconds(options.timestamp),
|
|
515
686
|
messageStubParameters: [],
|
|
516
|
-
participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
|
|
687
|
+
participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
|
|
517
688
|
status: WAMessageStatus.PENDING
|
|
518
|
-
})
|
|
519
|
-
}
|
|
689
|
+
})
|
|
690
|
+
}
|
|
520
691
|
|
|
521
692
|
export const generateWAMessage = async (jid, content, options = {}) => {
|
|
522
|
-
options.logger = options?.logger?.child({ msgId: options.messageId })
|
|
523
|
-
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ===== UTILITIES =====
|
|
527
|
-
export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown';
|
|
693
|
+
options.logger = options?.logger?.child({ msgId: options.messageId })
|
|
694
|
+
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
|
|
695
|
+
}
|
|
528
696
|
|
|
697
|
+
// ─── RECEIPTS / REACTIONS / POLLS ─────────────────────────────────────────────
|
|
529
698
|
export const updateMessageWithReceipt = (msg, receipt) => {
|
|
530
|
-
msg.userReceipt ||= []
|
|
531
|
-
const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid)
|
|
532
|
-
if (recp) Object.assign(recp, receipt)
|
|
533
|
-
else msg.userReceipt.push(receipt)
|
|
534
|
-
}
|
|
699
|
+
msg.userReceipt ||= []
|
|
700
|
+
const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid)
|
|
701
|
+
if (recp) Object.assign(recp, receipt)
|
|
702
|
+
else msg.userReceipt.push(receipt)
|
|
703
|
+
}
|
|
535
704
|
|
|
536
705
|
export const updateMessageWithReaction = (msg, reaction) => {
|
|
537
|
-
const authorID = getKeyAuthor(reaction.key)
|
|
538
|
-
msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID)
|
|
539
|
-
reaction.text ||= ''
|
|
540
|
-
msg.reactions.push(reaction)
|
|
541
|
-
}
|
|
706
|
+
const authorID = getKeyAuthor(reaction.key)
|
|
707
|
+
msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID)
|
|
708
|
+
reaction.text ||= ''
|
|
709
|
+
msg.reactions.push(reaction)
|
|
710
|
+
}
|
|
542
711
|
|
|
543
712
|
export const updateMessageWithPollUpdate = (msg, update) => {
|
|
544
|
-
const authorID = getKeyAuthor(update.pollUpdateMessageKey)
|
|
545
|
-
msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
|
|
546
|
-
if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update)
|
|
547
|
-
}
|
|
713
|
+
const authorID = getKeyAuthor(update.pollUpdateMessageKey)
|
|
714
|
+
msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
|
|
715
|
+
if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export const updateMessageWithEventResponse = (msg, update) => {
|
|
719
|
+
const authorID = getKeyAuthor(update.eventResponseMessageKey)
|
|
720
|
+
msg.eventResponses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID)
|
|
721
|
+
msg.eventResponses.push(update)
|
|
722
|
+
}
|
|
548
723
|
|
|
549
724
|
export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
|
|
550
|
-
const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
|
|
725
|
+
const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
|
|
551
726
|
const voteHashMap = opts.reduce((acc, opt) => {
|
|
552
|
-
|
|
553
|
-
acc
|
|
554
|
-
|
|
555
|
-
}, {});
|
|
556
|
-
|
|
727
|
+
acc[sha256(Buffer.from(opt.optionName || '')).toString()] = { name: opt.optionName || '', voters: [] }
|
|
728
|
+
return acc
|
|
729
|
+
}, {})
|
|
557
730
|
for (const update of pollUpdates || []) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
voteHashMap[hash]
|
|
563
|
-
voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
|
|
731
|
+
if (!update.vote) continue
|
|
732
|
+
for (const option of update.vote.selectedOptions || []) {
|
|
733
|
+
const hash = option.toString()
|
|
734
|
+
voteHashMap[hash] ||= { name: 'Unknown', voters: [] }
|
|
735
|
+
voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId))
|
|
564
736
|
}
|
|
565
737
|
}
|
|
566
|
-
return Object.values(voteHashMap)
|
|
738
|
+
return Object.values(voteHashMap)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function getAggregateResponsesInEventMessage({ eventResponses }, meId) {
|
|
742
|
+
const responseMap = { GOING: { response: 'GOING', responders: [] }, NOT_GOING: { response: 'NOT_GOING', responders: [] }, MAYBE: { response: 'MAYBE', responders: [] } }
|
|
743
|
+
for (const update of eventResponses || []) {
|
|
744
|
+
const type = update.eventResponse || 'UNKNOWN'
|
|
745
|
+
if (responseMap[type]) responseMap[type].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId))
|
|
746
|
+
}
|
|
747
|
+
return Object.values(responseMap)
|
|
567
748
|
}
|
|
568
749
|
|
|
569
750
|
export const aggregateMessageKeysNotFromMe = (keys) => {
|
|
570
|
-
const keyMap = {}
|
|
751
|
+
const keyMap = {}
|
|
571
752
|
for (const { remoteJid, id, participant, fromMe } of keys) {
|
|
572
753
|
if (!fromMe) {
|
|
573
|
-
const uqKey = `${remoteJid}:${participant || ''}
|
|
574
|
-
keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] }
|
|
575
|
-
keyMap[uqKey].messageIds.push(id)
|
|
754
|
+
const uqKey = `${remoteJid}:${participant || ''}`
|
|
755
|
+
keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] }
|
|
756
|
+
keyMap[uqKey].messageIds.push(id)
|
|
576
757
|
}
|
|
577
758
|
}
|
|
578
|
-
return Object.values(keyMap)
|
|
579
|
-
}
|
|
759
|
+
return Object.values(keyMap)
|
|
760
|
+
}
|
|
580
761
|
|
|
581
|
-
|
|
762
|
+
// ─── DOWNLOAD ─────────────────────────────────────────────────────────────────
|
|
763
|
+
const REUPLOAD_REQUIRED_STATUS = [410, 404]
|
|
582
764
|
|
|
583
765
|
export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
584
766
|
const downloadMsg = async () => {
|
|
585
|
-
let
|
|
586
|
-
if (!message.message && message.key
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message });
|
|
593
|
-
const contentType = getContentType(mContent);
|
|
594
|
-
let mediaType = contentType?.replace('Message', '');
|
|
595
|
-
const media = mContent[contentType];
|
|
767
|
+
let normalized = message
|
|
768
|
+
if (!message.message && message.key) normalized = { key: message.key, message: message.quoted?.message || message, messageTimestamp: message.messageTimestamp }
|
|
769
|
+
const mContent = extractMessageContent(normalized.message)
|
|
770
|
+
if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message })
|
|
771
|
+
const contentType = getContentType(mContent)
|
|
772
|
+
let mediaType = contentType?.replace('Message', '')
|
|
773
|
+
const media = mContent[contentType]
|
|
596
774
|
if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media)))
|
|
597
|
-
throw new Boom(`"${contentType}" is not a media message`)
|
|
598
|
-
const download = 'thumbnailDirectPath' in media && !('url' in media)
|
|
599
|
-
|
|
775
|
+
throw new Boom(`"${contentType}" message is not a media message`)
|
|
776
|
+
const download = ('thumbnailDirectPath' in media && !('url' in media))
|
|
777
|
+
? { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey }
|
|
778
|
+
: media
|
|
779
|
+
if ('thumbnailDirectPath' in media && !('url' in media)) mediaType = 'thumbnail-link'
|
|
780
|
+
const stream = await downloadContentFromMessage(download, mediaType, options)
|
|
600
781
|
if (type === 'buffer') {
|
|
601
|
-
const chunks = []
|
|
602
|
-
for await (const chunk of stream) chunks.push(chunk)
|
|
603
|
-
return Buffer.concat(chunks)
|
|
782
|
+
const chunks = []
|
|
783
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
784
|
+
return Buffer.concat(chunks)
|
|
604
785
|
}
|
|
605
|
-
return stream
|
|
606
|
-
}
|
|
786
|
+
return stream
|
|
787
|
+
}
|
|
607
788
|
return downloadMsg().catch(async (error) => {
|
|
608
|
-
if (ctx && typeof error?.status === 'number' &&
|
|
609
|
-
message
|
|
610
|
-
|
|
789
|
+
if (ctx && typeof error?.status === 'number' && REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
|
|
790
|
+
ctx.logger.info({ key: message.key }, 'sending reupload media request...')
|
|
791
|
+
message = await ctx.reuploadRequest(message)
|
|
792
|
+
return downloadMsg()
|
|
611
793
|
}
|
|
612
|
-
throw error
|
|
613
|
-
})
|
|
614
|
-
}
|
|
794
|
+
throw error
|
|
795
|
+
})
|
|
796
|
+
}
|
|
615
797
|
|
|
616
|
-
export
|
|
617
|
-
|
|
618
|
-
|
|
798
|
+
export const assertMediaContent = (content) => {
|
|
799
|
+
content = extractMessageContent(content)
|
|
800
|
+
const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage
|
|
801
|
+
if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content })
|
|
802
|
+
return mediaContent
|
|
803
|
+
}
|
|
619
804
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
const validStickers = [];
|
|
805
|
+
// ─── DEVICE / MD UTILS ────────────────────────────────────────────────────────
|
|
806
|
+
export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown'
|
|
623
807
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (!isWebP) {
|
|
630
|
-
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp().toBuffer();
|
|
631
|
-
else if ('jimp' in lib) buffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'));
|
|
632
|
-
}
|
|
633
|
-
if (buffer.length > 1024 * 1024) {
|
|
634
|
-
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer();
|
|
635
|
-
if (buffer.length > 1024 * 1024) continue;
|
|
808
|
+
export const patchMessageForMdIfRequired = (message) => {
|
|
809
|
+
if (message?.buttonsMessage || message?.templateMessage || message?.listMessage || message?.interactiveMessage?.nativeFlowMessage) {
|
|
810
|
+
message = {
|
|
811
|
+
viewOnceMessageV2Extension: {
|
|
812
|
+
message: { messageContextInfo: { deviceListMetadataVersion: 2, deviceListMetadata: {} }, ...message }
|
|
636
813
|
}
|
|
637
|
-
validStickers.push({
|
|
638
|
-
fileName: `${sha256(buffer).toString('base64').replace(/\//g, '-')}.webp`,
|
|
639
|
-
buffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false,
|
|
640
|
-
emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel
|
|
641
|
-
});
|
|
642
|
-
} catch (e) { options.logger?.warn(`Sticker failed: ${e.message}`); }
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (!validStickers.length) throw new Boom('No valid stickers', { statusCode: 400 });
|
|
646
|
-
|
|
647
|
-
const { stream: covStream } = await getStream(stickerPack.cover);
|
|
648
|
-
let coverBuffer = await toBuffer(covStream);
|
|
649
|
-
const isWebPCover = coverBuffer.length >= 12 && coverBuffer[0] === 0x52 && coverBuffer[1] === 0x49 && coverBuffer[2] === 0x46 && coverBuffer[3] === 0x46;
|
|
650
|
-
if (!isWebPCover) {
|
|
651
|
-
if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
|
|
652
|
-
else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const processBatch = async (batch, batchIdx) => {
|
|
656
|
-
const batchData = {};
|
|
657
|
-
batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
|
|
658
|
-
const trayFile = `${packId_}_batch${batchIdx}.webp`;
|
|
659
|
-
batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
|
|
660
|
-
|
|
661
|
-
const zipBuf = await new Promise((resolve, reject) => { zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))); });
|
|
662
|
-
const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
|
|
663
|
-
const uploadRes = await options.upload(upload.encFilePath, {
|
|
664
|
-
fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
665
|
-
});
|
|
666
|
-
await fs.unlink(upload.encFilePath);
|
|
667
|
-
|
|
668
|
-
let thumbBuf;
|
|
669
|
-
if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
|
|
670
|
-
else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
|
|
671
|
-
|
|
672
|
-
let thumbUploadRes;
|
|
673
|
-
if (thumbBuf?.length) {
|
|
674
|
-
const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
|
|
675
|
-
thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
|
|
676
|
-
fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
677
|
-
});
|
|
678
|
-
await fs.unlink(thumbUpload.encFilePath);
|
|
679
814
|
}
|
|
680
|
-
|
|
681
|
-
return {
|
|
682
|
-
name: `${name} (${batchIdx + 1})`, publisher, packDescription: description, stickerPackId: `${packId_}_${batchIdx}`,
|
|
683
|
-
stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
|
|
684
|
-
stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
|
|
685
|
-
fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
|
|
686
|
-
directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
|
|
687
|
-
...(thumbUploadRes && { thumbnailDirectPath: thumbUploadRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined })
|
|
688
|
-
};
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
if (validStickers.length > 60) {
|
|
692
|
-
const batches = [];
|
|
693
|
-
for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
|
|
694
|
-
const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
|
|
695
|
-
return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
|
|
696
815
|
}
|
|
697
|
-
|
|
698
|
-
return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
|
|
816
|
+
return message
|
|
699
817
|
}
|
|
700
818
|
|
|
701
|
-
export const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content });
|
|
705
|
-
return mediaContent;
|
|
706
|
-
};
|
|
819
|
+
export const hasNonNullishProperty = (message, key) => typeof message === 'object' && message !== null && key in message && message[key] !== null && message[key] !== undefined
|
|
820
|
+
|
|
821
|
+
export const hasOptionalProperty = (obj, key) => typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null
|