@zervonex/baileys 0.0.0-stage → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +316 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. package/package.json +216 -5
@@ -0,0 +1,742 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import { Sticker, StickerTypes } from 'wa-sticker-formatter'
3
+ import sharp from 'sharp'
4
+ import { fileTypeFromBuffer } from 'file-type'
5
+ import ffmpegStatic from 'ffmpeg-static'
6
+ import { randomBytes } from 'crypto'
7
+ import { promises as fs, readFileSync, mkdtempSync, writeFileSync, rmSync } from 'fs'
8
+ import { gunzipSync } from 'zlib'
9
+ import path from 'path'
10
+ import { tmpdir } from './messages-media.js'
11
+ import { execFile } from 'child_process'
12
+ import { zip } from 'fflate'
13
+ import { createRequire } from 'module'
14
+ import { proto } from '../../WAProto/index.js'
15
+ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
16
+ import { WAMessageStatus, WAProto } from '../Types/index.js'
17
+ import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js'
18
+ import { sha256 } from './crypto.js'
19
+ import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js'
20
+ import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getStream, toBuffer, normalizeToBuffer } from './messages-media.js'
21
+ import { shouldIncludeReportingToken } from './reporting-utils.js'
22
+
23
+ const require = createRequire(import.meta.url)
24
+ const { Exif: StickerExif } = require('wa-sticker-formatter')
25
+ if (ffmpegStatic) process.env.FFMPEG_PATH = ffmpegStatic
26
+
27
+ // ─── CONSTANTS ────────────────────────────────────────────────────────────────
28
+
29
+ // Default mimetypes — these are WA protocol constants, not proto schema, so a small map is correct
30
+ const MIMETYPE_DEFAULTS = { image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf', audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg' }
31
+
32
+ // High-level user-facing send keys that are NOT proto field names — used only to detect pass-through
33
+ const HIGH_LEVEL_KEYS = new Set(['text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location', 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack', 'pin', 'buttonReply', 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin', 'requestPayment', 'sharePhoneNumber', 'requestPhoneNumber', 'limitSharing', 'viewOnce', 'mentions', 'edit', 'buttons', 'templateButtons', 'sections', 'interactiveButtons', 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop', 'payment', 'collection'])
34
+
35
+ // Wrapper field names for normalizeMessageContent — derived from what WA wraps messages in
36
+ const WRAPPER_KEYS = ['ephemeralMessage', 'viewOnceMessage', 'documentWithCaptionMessage', 'viewOnceMessageV2', 'viewOnceMessageV2Extension', 'editedMessage', 'groupMentionedMessage', 'botInvokeMessage', 'lottieStickerMessage', 'eventCoverImage', 'statusMentionMessage', 'pollCreationOptionImageMessage', 'associatedChildMessage', 'groupStatusMentionMessage', 'pollCreationMessageV4', 'pollCreationMessageV5', 'statusAddYours', 'groupStatusMessage', 'limitSharingMessage', 'botTaskMessage', 'questionMessage', 'botForwardedMessage']
37
+
38
+ const REUPLOAD_REQUIRED_STATUS = [410, 404]
39
+ const STICKER_MAX_BYTES = 1_000_000
40
+ const CIRCLE_GEQ = `clip(256-hypot(X-W/2\\,Y-H/2)\\,0\\,1)*255`
41
+ const ROUNDED_GEQ = `st(0\\,abs(X-W/2)-W/2+50);st(1\\,abs(Y-H/2)-H/2+50);st(2\\,hypot(max(ld(0)\\,0)\\,max(ld(1)\\,0))+min(max(ld(0)\\,ld(1))\\,0)-50);clip(-ld(2)\\,0\\,1)*255`
42
+ const BASE_CROP_VF = 'scale=512:512:force_original_aspect_ratio=increase,crop=512:512:(iw-512)/2:(ih-512)/2,fps=8'
43
+ const BASE_FULL_VF = 'scale=512:512,fps=8'
44
+ const WEBP_ANIM_CODEC = ['-vcodec', 'libwebp_anim', '-lossless', '0', '-compression_level', '4', '-q:v', '35', '-loop', '0', '-preset', 'default']
45
+ const WEBP_STATIC_CODEC = (q) => ['-vcodec', 'libwebp', '-lossless', '0', '-compression_level', '6', '-q:v', String(q), '-loop', '0', '-preset', 'picture']
46
+
47
+ // ─── STICKER ──────────────────────────────────────────────────────────────────
48
+
49
+ const ffmpegRun = (args) => new Promise((resolve, reject) => execFile(ffmpegStatic, args, { timeout: 120_000 }, (err) => err ? reject(err) : resolve()))
50
+
51
+ const buildArgs = async (stickerType, inPath, outPath, extraIn = [], codecArgs = []) => {
52
+ const isShape = stickerType === StickerTypes.CIRCLE || stickerType === StickerTypes.ROUNDED
53
+ if (isShape) {
54
+ const geq = stickerType === StickerTypes.CIRCLE ? CIRCLE_GEQ : ROUNDED_GEQ
55
+ return ['-y', '-threads', '0', ...extraIn, '-i', inPath, '-filter_complex', `[0:v]${BASE_CROP_VF},format=rgba,geq=r='r(X\\,Y)':g='g(X\\,Y)':b='b(X\\,Y)':a='${geq}'[out]`, '-map', '[out]', ...codecArgs, '-an', outPath]
56
+ }
57
+ const vf = stickerType === StickerTypes.DEFAULT ? null : stickerType === StickerTypes.FULL ? BASE_FULL_VF : BASE_CROP_VF
58
+ return ['-y', '-threads', '0', ...extraIn, '-i', inPath, ...(vf ? ['-vf', vf] : []), ...codecArgs, '-an', outPath]
59
+ }
60
+
61
+ const compressWebp = async (buf, { animated, quality = 80, stickerType = StickerTypes.ROUNDED, packName, authorName } = {}) => {
62
+ const detected = await fileTypeFromBuffer(buf)
63
+ const cuid = generateMessageIDV2()
64
+ const videoExts = new Set(['mp4', 'webm', 'mkv', 'avi', 'mov', 'flv', 'gif'])
65
+ if (detected && videoExts.has(detected.ext)) {
66
+ const tmpIn = path.join(tmpdir(), `stk_in_${cuid}.${detected.ext}`)
67
+ const tmpOut = path.join(tmpdir(), `stk_out_${cuid}.webp`)
68
+ try {
69
+ writeFileSync(tmpIn, buf)
70
+ await ffmpegRun(await buildArgs(stickerType, tmpIn, tmpOut, ['-t', '10'], WEBP_ANIM_CODEC))
71
+ const out = readFileSync(tmpOut)
72
+ return { buffer: packName ? await tagExif(await shrinkToLimit(out, { animated: true }), packName, authorName) : out, isAnimated: true }
73
+ } finally { fs.unlink(tmpIn).catch(() => { }); fs.unlink(tmpOut).catch(() => { }) }
74
+ }
75
+ if (detected?.mime === 'image/webp') {
76
+ const meta = await sharp(buf, { animated: true }).metadata()
77
+ const isAnim = animated ?? ((meta.pages || 1) > 1)
78
+ const tmpOut = path.join(tmpdir(), `stk_out_${cuid}.webp`)
79
+ if (isAnim) {
80
+ try {
81
+ await sharp(buf, { animated: true }).webp({ quality, effort: 1, loop: 0 }).toFile(tmpOut)
82
+ const out = readFileSync(tmpOut)
83
+ return { buffer: packName ? await tagExif(await shrinkToLimit(out, { animated: true }), packName, authorName) : out, isAnimated: true }
84
+ } finally { fs.unlink(tmpOut).catch(() => { }) }
85
+ }
86
+ const tmpIn = path.join(tmpdir(), `stk_in_${cuid}.webp`)
87
+ try {
88
+ writeFileSync(tmpIn, buf)
89
+ await ffmpegRun(await buildArgs(stickerType, tmpIn, tmpOut, [], WEBP_STATIC_CODEC(quality)))
90
+ const out = readFileSync(tmpOut)
91
+ return { buffer: packName ? await tagExif(await shrinkToLimit(out, { animated: false }), packName, authorName) : out, isAnimated: false }
92
+ } finally { fs.unlink(tmpIn).catch(() => { }); fs.unlink(tmpOut).catch(() => { }) }
93
+ }
94
+ const converted = await new Sticker(buf, { pack: packName, author: authorName, type: stickerType, quality }).toBuffer()
95
+ return { buffer: converted.length <= STICKER_MAX_BYTES ? converted : await shrinkToLimit(converted, { animated: false }), isAnimated: false }
96
+ }
97
+
98
+ const shrinkToLimit = async (buf, { animated } = {}) => {
99
+ if (buf.length <= STICKER_MAX_BYTES) return buf
100
+ const ratio = STICKER_MAX_BYTES / buf.length
101
+ const q = Math.max(10, Math.min(60, Math.floor(ratio * 80 * 0.85)))
102
+ const out = (await compressWebp(buf, { animated, quality: q })).buffer
103
+ if (out.length <= STICKER_MAX_BYTES) return out
104
+ const q2 = Math.max(10, Math.floor(q * (STICKER_MAX_BYTES / out.length) * 0.85))
105
+ const out2 = (await compressWebp(buf, { animated, quality: q2 })).buffer
106
+ return out.length < out2.length ? out : out2
107
+ }
108
+
109
+ const tagExif = async (buf, packName, authorName) => new StickerExif({ pack: packName, author: authorName }).add(buf)
110
+
111
+ let _rlottieApi = null
112
+ const getRlottieApi = async () => {
113
+ if (_rlottieApi) return _rlottieApi
114
+ const { init } = await import('rlottie')
115
+ const wasmBuf = readFileSync(require.resolve('rlottie/wasm'))
116
+ _rlottieApi = await init('data:application/wasm;base64,' + wasmBuf.toString('base64'))
117
+ return _rlottieApi
118
+ }
119
+
120
+ const tgsToWebp = async (tgsBuffer, { quality = 80, fps = 30 } = {}) => {
121
+ const lottieJson = JSON.parse(gunzipSync(tgsBuffer).toString())
122
+ const api = await getRlottieApi()
123
+ const W = 512, H = 512, handle = api.lottie_init()
124
+ const jsonBuf = Buffer.from(JSON.stringify(lottieJson) + '\0')
125
+ const ptr = api._malloc(jsonBuf.length)
126
+ api.HEAPU8.set(jsonBuf, ptr)
127
+ const totalFrames = api.lottie_load_from_data(handle, ptr)
128
+ api.lottie_resize(handle, W, H)
129
+ const bufPtr = api.lottie_buffer(handle)
130
+ const tgsTmpDir = mkdtempSync(path.join(tmpdir(), 'tgs_'))
131
+ try {
132
+ for (let i = 0; i < totalFrames; i++) {
133
+ api.lottie_render(handle, i)
134
+ const rgba = Buffer.from(api.HEAPU8.buffer, bufPtr, W * H * 4)
135
+ await sharp(Buffer.from(rgba), { raw: { width: W, height: H, channels: 4 } }).png().toFile(path.join(tgsTmpDir, `f${String(i).padStart(4, '0')}.png`))
136
+ }
137
+ api.lottie_destroy(handle)
138
+ const outPath = path.join(tgsTmpDir, 'out.webp')
139
+ await ffmpegRun(['-y', '-framerate', String(fps), '-i', path.join(tgsTmpDir, 'f%04d.png'), '-vcodec', 'libwebp', '-vf', `scale=${W}:${H}`, '-lossless', '0', '-compression_level', '6', '-q:v', String(quality), '-loop', '0', '-preset', 'default', '-an', '-vsync', '0', outPath])
140
+ return readFileSync(outPath)
141
+ } finally { rmSync(tgsTmpDir, { recursive: true, force: true }) }
142
+ }
143
+
144
+ // ─── EXPORTS: URL / LINK PREVIEW ─────────────────────────────────────────────
145
+
146
+ export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0]
147
+
148
+ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
149
+ const url = extractUrlFromText(text)
150
+ if (!getUrlInfo || !url) return
151
+ try { return await getUrlInfo(url) } catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed') }
152
+ }
153
+
154
+ // ─── EXPORTS: MESSAGE HELPERS ─────────────────────────────────────────────────
155
+
156
+ export const getContentType = (content) => {
157
+ if (!content) return
158
+ return Object.keys(content).find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage')
159
+ }
160
+
161
+ export const normalizeMessageContent = (content) => {
162
+ if (!content) return
163
+ for (let i = 0; i < 5; i++) {
164
+ const inner = WRAPPER_KEYS.reduce((found, k) => found || content?.[k], null)
165
+ if (!inner) break
166
+ content = inner.message
167
+ }
168
+ return content
169
+ }
170
+
171
+ export const extractMessageContent = (content) => {
172
+ content = normalizeMessageContent(content)
173
+ for (const t of ['image', 'video', 'audio', 'document']) { if (content?.[`${t}Message`]?.viewOnce) return { [`${t}Message`]: content[`${t}Message`] } }
174
+ const fromButtons = (msg) => {
175
+ for (const t of ['image', 'document', 'video', 'location', 'product']) { if (msg[`${t}Message`]) return { [`${t}Message`]: msg[`${t}Message`] } }
176
+ return { conversation: msg.contentText || msg.hydratedContentText || msg.body?.text || '' }
177
+ }
178
+ if (content?.buttonsMessage) return fromButtons(content.buttonsMessage)
179
+ if (content?.interactiveMessage) return fromButtons(content.interactiveMessage)
180
+ for (const v of ['interactiveMessageTemplate', 'hydratedFourRowTemplate', 'hydratedTemplate', 'fourRowTemplate']) {
181
+ if (content?.templateMessage?.[v]) return fromButtons(content.templateMessage[v])
182
+ }
183
+ return content
184
+ }
185
+
186
+ export const generateForwardMessageContent = (message, forceForward) => {
187
+ let content = normalizeMessageContent(message.message)
188
+ if (!content) throw new Boom('no content in message', { statusCode: 400 })
189
+ content = proto.Message.decode(proto.Message.encode(content).finish())
190
+ let key = Object.keys(content)[0]
191
+ const score = (content?.[key]?.contextInfo?.forwardingScore || 0) + (message.key.fromMe && !forceForward ? 0 : 1)
192
+ if (key === 'conversation') { content.extendedTextMessage = { text: content[key] }; delete content.conversation; key = 'extendedTextMessage' }
193
+ content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {}
194
+ return content
195
+ }
196
+
197
+ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
198
+ ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
199
+ })
200
+
201
+ // ─── EXPORTS: MEDIA ───────────────────────────────────────────────────────────
202
+
203
+ export const prepareWAMessageMedia = async (message, options) => {
204
+ const mediaType = MEDIA_KEYS.find(k => k in message)
205
+ if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
206
+ const protoKey = `${mediaType}Message`
207
+ const uploadData = { ...message, media: message[mediaType] }
208
+ delete uploadData[mediaType]
209
+ if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
210
+ if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_DEFAULTS[mediaType] || 'application/octet-stream'
211
+
212
+ if (mediaType === 'sticker') {
213
+ try {
214
+ const rawBuf = await toBuffer((await getStream(uploadData.media)).stream)
215
+ const packName = message.pack || message.packName || options?.pack || options?.packName || 'ZervonexStickers'
216
+ const authorName = message.author || message.publisher || message.packPublisher || options?.author || options?.publisher || options?.packPublisher || 'Zervonex'
217
+ const { buffer } = await compressWebp(rawBuf, { packName, authorName, stickerType: message.stickerType || options?.stickerType || StickerTypes.ROUNDED, quality: message.quality || options?.quality || 80 })
218
+ uploadData.media = buffer
219
+ uploadData.stickerSentTs = Date.now()
220
+ } catch (e) { options.logger?.warn({ err: e }, 'sticker formatting failed, sending raw') }
221
+ }
222
+
223
+ const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache
224
+ ? `${mediaType}:${uploadData.media.url.toString()}` : null
225
+ if (cacheableKey) {
226
+ const cached = await options.mediaCache?.get(cacheableKey)
227
+ if (cached) {
228
+ const obj = WAProto.Message.decode(cached)
229
+ Object.assign(obj[protoKey], { ...uploadData, media: undefined })
230
+ return obj
231
+ }
232
+ }
233
+
234
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid)
235
+ const requiresDuration = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
236
+ const requiresThumbnail = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
237
+ if (uploadData.jpegThumbnail) uploadData.jpegThumbnail = await normalizeToBuffer(uploadData.jpegThumbnail)
238
+ const requiresWaveform = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor)
239
+
240
+ const enc = await (isNewsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
241
+ logger: options.logger, saveOriginalFileIfRequired: requiresDuration || requiresThumbnail || requiresWaveform,
242
+ opts: options.options, isPtt: uploadData.ptt, forceOpus: mediaType === 'audio' && uploadData.mimetype?.includes('opus'), convertVideo: mediaType === 'video',
243
+ })
244
+ const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, opusConverted, encFilePath, encBuffer, cleanup } = enc
245
+ if (mediaType === 'audio' && opusConverted) uploadData.mimetype = 'audio/ogg; codecs=opus'
246
+ const fileEncSha256B64 = (isNewsletter ? fileSha256 : (fileEncSha256 ?? fileSha256)).toString('base64')
247
+
248
+ const [{ mediaUrl, directPath, handle }] = await Promise.all([
249
+ options.upload(encFilePath || encBuffer || encWriteStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs }),
250
+ (async () => {
251
+ try {
252
+ if (requiresThumbnail) {
253
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(bodyPath, mediaType, options)
254
+ uploadData.jpegThumbnail = thumbnail
255
+ if (!uploadData.width && originalImageDimensions) { uploadData.width = originalImageDimensions.width; uploadData.height = originalImageDimensions.height }
256
+ }
257
+ if (requiresDuration) uploadData.seconds = await getAudioDuration(bodyPath)
258
+ if (requiresWaveform) {
259
+ try { uploadData.waveform = await getAudioWaveform(bodyPath, options.logger) } catch {
260
+ 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])
261
+ }
262
+ }
263
+ if (options.backgroundColor && mediaType === 'audio') uploadData.backgroundArgb = assertColor(options.backgroundColor)
264
+ } catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info') }
265
+ })()
266
+ ]).finally(async () => {
267
+ if (encWriteStream && !Buffer.isBuffer(encWriteStream)) encWriteStream.destroy?.()
268
+ if (cleanup) await cleanup()
269
+ })
270
+
271
+ // Resolve proto constructor dynamically from proto.Message — no hardcoded map
272
+ const CtorName = mediaType.charAt(0).toUpperCase() + mediaType.slice(1) + 'Message'
273
+ const Ctor = WAProto.Message[CtorName] || { fromObject: o => o }
274
+ const obj = WAProto.Message.fromObject({
275
+ [protoKey]: Ctor.fromObject({ url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength, mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined })
276
+ })
277
+ if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
278
+ if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
279
+ return obj
280
+ }
281
+
282
+ const assertColor = (color) => {
283
+ if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1
284
+ const hex = color.trim().replace('#', '')
285
+ return parseInt(hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex, 16)
286
+ }
287
+
288
+ // ─── EXPORTS: STICKER PACK ────────────────────────────────────────────────────
289
+
290
+ export const prepareStickerPackMessage = async (stickerPack, options) => {
291
+ if (Array.isArray(stickerPack)) stickerPack = { stickers: stickerPack }
292
+ else if (stickerPack && typeof stickerPack === 'object' && !stickerPack.stickers) {
293
+ const keys = Object.keys(stickerPack)
294
+ if (keys.length && keys.every(k => !isNaN(k))) stickerPack = { stickers: Object.values(stickerPack) }
295
+ }
296
+ const { stickers, cover, name, publisher, packId, packName: packNameAlias, packPublisher, author } = stickerPack
297
+ if (!stickers?.length) throw new Boom('Sticker pack must contain at least one sticker', { statusCode: 400 })
298
+ const stickerPackIdValue = packId || generateMessageIDV2()
299
+ const packName = name || packNameAlias || options?.packName || options?.name || 'ZervonexStickers'
300
+ const authorName = publisher || packPublisher || author || options?.packPublisher || options?.publisher || options?.author || 'Zervonex'
301
+ const MAX_STICKERS_PER_PACK = 60, skippedStickers = []
302
+ const runWithLimit = (() => {
303
+ const limit = 6, queue = []
304
+ let running = 0
305
+ return (fn) => new Promise((resolve, reject) => {
306
+ const run = () => { running++; fn().then(resolve, reject).finally(() => { running--; queue.shift()?.() }) }
307
+ running < limit ? run() : queue.push(run)
308
+ })
309
+ })()
310
+
311
+ const processSticker = async (s, i) => {
312
+ const uid = generateMessageIDV2()
313
+ const tmpOut = path.join(tmpdir(), `stk_out_${i}_${uid}.webp`)
314
+ try {
315
+ const raw = s.data || s.sticker || s.buffer || s.image || s.webp || s.file || s.path || s.url
316
+ if (!raw) { skippedStickers.push({ index: i, reason: 'No sticker data found' }); return null }
317
+ const buf = Buffer.isBuffer(raw) ? raw : await toBuffer((await getStream(raw)).stream)
318
+ if (!buf?.length) { skippedStickers.push({ index: i, reason: 'Empty buffer' }); return null }
319
+ const emojis = Array.isArray(s.emojis) ? s.emojis : Object.values(s.emojis || {})
320
+ let finalBuf
321
+ if (s.isLottie) {
322
+ finalBuf = await tagExif(await shrinkToLimit(await tgsToWebp(buf, { quality: 80, fps: 30 }), { animated: true }), packName, authorName)
323
+ } else if (s.isAnimated) {
324
+ const tmpIn = path.join(tmpdir(), `stk_in_${i}_${uid}.webp`), tmpRaw = path.join(tmpdir(), `stk_raw_${i}_${uid}.webp`)
325
+ writeFileSync(tmpIn, buf)
326
+ try {
327
+ await ffmpegRun(await buildArgs(s.type || StickerTypes.ROUNDED, tmpIn, tmpRaw, ['-vcodec', 'vp9', '-t', '10'], WEBP_ANIM_CODEC))
328
+ finalBuf = await tagExif(await shrinkToLimit(readFileSync(tmpRaw), { animated: true }), packName, authorName)
329
+ } finally { fs.unlink(tmpIn).catch(() => { }); fs.unlink(tmpRaw).catch(() => { }) }
330
+ } else {
331
+ finalBuf = await shrinkToLimit(await new Sticker(buf, { pack: packName, author: authorName, type: s.type || StickerTypes.ROUNDED, quality: 80 }).toBuffer(), { animated: false })
332
+ }
333
+ const hash = sha256(finalBuf).toString('base64').replace(/\//g, '-').replace(/=/g, '')
334
+ const fileSize = finalBuf.length
335
+ writeFileSync(tmpOut, finalBuf); finalBuf = null
336
+ return { fileName: `${hash}.webp`, filePath: tmpOut, fileSize, mimetype: 'image/webp', isAnimated: s.isAnimated || false, isLottie: s.isLottie || false, emojis, accessibilityLabel: s.accessibilityLabel || '' }
337
+ } catch (e) {
338
+ options.logger?.warn({ err: e }, `failed processing sticker at index ${i}`)
339
+ skippedStickers.push({ index: i, reason: e.message }); fs.unlink(tmpOut).catch(() => { }); return null
340
+ }
341
+ }
342
+
343
+ const processBatch = async (batch, batchIdx, totalBatches, coverFilePath) => {
344
+ const batchData = {}
345
+ for (const s of batch) batchData[s.fileName] = [new Uint8Array(readFileSync(s.filePath)), { level: 0 }]
346
+ const trayFile = totalBatches > 1 ? `${stickerPackIdValue}_batch${batchIdx}.webp` : `${stickerPackIdValue}.webp`
347
+ batchData[trayFile] = [new Uint8Array(readFileSync(coverFilePath)), { level: 0 }]
348
+ const zipBuf = await new Promise((resolve, reject) => zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))))
349
+ const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options })
350
+ const uploadRes = await options.upload(upload.encFilePath || upload.encBuffer, { fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs })
351
+ if (upload.encFilePath) fs.unlink(upload.encFilePath).catch(() => { })
352
+ let thumbRes = null
353
+ try {
354
+ const thumbTmpPath = path.join(tmpdir(), `stk_thumb_${generateMessageIDV2()}.jpg`)
355
+ await sharp(coverFilePath).resize(252, 252).jpeg().toFile(thumbTmpPath)
356
+ const thumbBuf = readFileSync(thumbTmpPath); fs.unlink(thumbTmpPath).catch(() => { })
357
+ const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey })
358
+ thumbRes = await options.upload(thumbUpload.encFilePath || thumbUpload.encBuffer, { fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs })
359
+ if (thumbUpload.encFilePath) fs.unlink(thumbUpload.encFilePath).catch(() => { })
360
+ thumbRes._enc = thumbUpload; thumbRes._thumbBuf = thumbBuf
361
+ } catch (e) { options.logger?.warn({ err: e }, 'failed generating sticker pack thumbnail') }
362
+ return {
363
+ name: totalBatches > 1 ? `${packName} (${batchIdx + 1}/${totalBatches})` : packName, publisher: authorName,
364
+ stickerPackId: totalBatches > 1 ? `${stickerPackIdValue}_${batchIdx}` : stickerPackIdValue,
365
+ stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
366
+ stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
367
+ fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
368
+ directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
369
+ ...(thumbRes && { thumbnailDirectPath: thumbRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, thumbnailSha256: thumbRes._enc?.fileSha256, thumbnailEncSha256: thumbRes._enc?.fileEncSha256, imageDataHash: thumbRes._thumbBuf ? sha256(thumbRes._thumbBuf).toString('base64') : undefined }),
370
+ }
371
+ }
372
+
373
+ const coverTmpPath = path.join(tmpdir(), `stk_cover_${generateMessageIDV2()}.webp`)
374
+ let coverFilePath
375
+ try {
376
+ if (cover) {
377
+ const coverBuf = Buffer.isBuffer(cover) ? cover : await toBuffer((await getStream(cover)).stream)
378
+ const isWebp = coverBuf[0] === 0x52 && coverBuf[1] === 0x49 && coverBuf[8] === 0x57 && coverBuf[9] === 0x45
379
+ writeFileSync(coverTmpPath, isWebp ? coverBuf : await new Sticker(coverBuf, { pack: packName, author: authorName, type: StickerTypes.ROUNDED, quality: 95 }).toBuffer())
380
+ coverFilePath = coverTmpPath
381
+ }
382
+ const allProcessed = (await Promise.all(stickers.map((s, j) => runWithLimit(() => processSticker(s, j))))).filter(Boolean)
383
+ if (!coverFilePath && allProcessed.length) coverFilePath = allProcessed[0].filePath
384
+ const sizeBatches = []
385
+ let curBatch = []
386
+ for (const s of allProcessed) { if (curBatch.length >= MAX_STICKERS_PER_PACK) { sizeBatches.push(curBatch); curBatch = [] }; curBatch.push(s) }
387
+ if (curBatch.length) sizeBatches.push(curBatch)
388
+ const totalBatches = sizeBatches.length
389
+ const allResults = await Promise.all(sizeBatches.map((batch, idx) => processBatch(batch, idx, totalBatches, coverFilePath).finally(() => { for (const s of batch) fs.unlink(s.filePath).catch(() => { }) })))
390
+ if (!allResults.length) throw new Boom('No valid stickers could be processed', { statusCode: 400 })
391
+ return allResults.length > 1 ? { stickerPackMessage: allResults, isBatched: true, batchCount: allResults.length } : { stickerPackMessage: allResults[0], isBatched: false }
392
+ } finally { fs.unlink(coverTmpPath).catch(() => { }) }
393
+ }
394
+
395
+ // ─── MESSAGE CONTENT GENERATION ───────────────────────────────────────────────
396
+
397
+ // Handler dispatch: each key maps to an async fn(message, options) -> raw proto-compatible object.
398
+ // To add a new message type: add one entry here. No else-if chains, no declarations elsewhere.
399
+ // Proto enum values come directly from proto.Message.* — nothing is hardcoded.
400
+ const MESSAGE_HANDLERS = {
401
+ text: async (msg, opts) => {
402
+ const ext = { text: msg.text }
403
+ let urlInfo = msg.linkPreview ?? await generateLinkPreviewIfRequired(msg.text, opts.getUrlInfo, opts.logger)
404
+ if (urlInfo) {
405
+ Object.assign(ext, { matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail, description: urlInfo.description, title: urlInfo.title, previewType: urlInfo.previewType ?? 0 })
406
+ const img = urlInfo.highQualityThumbnail
407
+ if (img) Object.assign(ext, { thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp, thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256 })
408
+ }
409
+ if (opts.backgroundColor) ext.backgroundArgb = assertColor(opts.backgroundColor)
410
+ if (opts.font) ext.font = opts.font
411
+ return { extendedTextMessage: ext }
412
+ },
413
+ contacts: async (msg) => {
414
+ const { contacts } = msg.contacts
415
+ if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 })
416
+ return contacts.length === 1 ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) } : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(msg.contacts) }
417
+ },
418
+ location: async (msg) => ({ locationMessage: WAProto.Message.LocationMessage.create(msg.location) }),
419
+ react: async (msg) => { if (!msg.react.senderTimestampMs) msg.react.senderTimestampMs = Date.now(); return { reactionMessage: WAProto.Message.ReactionMessage.create(msg.react) } },
420
+ delete: async (msg) => ({ protocolMessage: { key: msg.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } }),
421
+ forward: async (msg) => generateForwardMessageContent(msg.forward, msg.force),
422
+ disappearingMessagesInChat: async (msg) => prepareDisappearingMessageSettingContent(typeof msg.disappearingMessagesInChat === 'boolean' ? (msg.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : msg.disappearingMessagesInChat),
423
+ groupInvite: async (msg, opts) => {
424
+ const m = { groupInviteMessage: { inviteCode: msg.groupInvite.inviteCode, inviteExpiration: msg.groupInvite.inviteExpiration, caption: msg.groupInvite.text, groupJid: msg.groupInvite.jid, groupName: msg.groupInvite.subject } }
425
+ if (opts.getProfilePicUrl) {
426
+ const pfpUrl = await opts.getProfilePicUrl(msg.groupInvite.jid, 'preview')
427
+ if (pfpUrl) { const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: opts?.options?.dispatcher }); if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer()) }
428
+ }
429
+ return m
430
+ },
431
+ stickerPack: async (msg, opts) => {
432
+ const result = await prepareStickerPackMessage(msg.stickerPack, opts)
433
+ if (result.isBatched) return { _batched: true, stickerPackMessage: result.stickerPackMessage, batchCount: result.batchCount }
434
+ return { _finalize: true, stickerPackMessage: result.stickerPackMessage }
435
+ },
436
+ pin: async (msg, opts) => {
437
+ const messageKey = typeof msg.pin === 'boolean' ? (opts.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation') })()) : typeof msg.pin === 'object' ? (msg.pin.key || (msg.pin.id ? { remoteJid: opts.jid, fromMe: msg.pin.fromMe || false, id: msg.pin.id, participant: msg.pin.participant } : null)) : msg.pin
438
+ if (!messageKey?.id) throw new Boom('Invalid message key for pin operation')
439
+ const shouldPin = typeof msg.pin === 'boolean' ? msg.pin : (msg.pin?.unpin !== true)
440
+ return { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (msg.pin?.time || msg.time || 86400) : 0 } }
441
+ },
442
+ keep: async (msg) => ({ keepInChatMessage: { key: msg.keep, keepType: msg.type, timestampMs: Date.now() } }),
443
+ call: async (msg) => ({ scheduledCallCreationMessage: { scheduledTimestampMs: msg.call.time || Date.now(), callType: msg.call.type || 1, title: msg.call.title } }),
444
+ paymentInvite: async (msg) => ({ paymentInviteMessage: { serviceType: msg.paymentInvite.type, expiryTimestamp: msg.paymentInvite.expiry } }),
445
+ buttonReply: async (msg) => ({
446
+ list: { listResponseMessage: { title: msg.buttonReply.title, description: msg.buttonReply.description, singleSelectReply: { selectedRowId: msg.buttonReply.rowId }, lisType: proto.Message.ListResponseMessage.ListType.SINGLE_SELECT } },
447
+ template: { templateButtonReplyMessage: { selectedDisplayText: msg.buttonReply.displayText, selectedId: msg.buttonReply.id, selectedIndex: msg.buttonReply.index } },
448
+ interactive: { interactiveResponseMessage: { body: { text: msg.buttonReply.displayText, format: proto.Message.InteractiveResponseMessage.Body.Format.EXTENSIONS_1 }, nativeFlowResponseMessage: { name: msg.buttonReply.nativeFlows?.name, paramsJson: msg.buttonReply.nativeFlows?.paramsJson, version: msg.buttonReply.nativeFlows?.version } } },
449
+ }[msg.type] || { buttonsResponseMessage: { selectedButtonId: msg.buttonReply.id, selectedDisplayText: msg.buttonReply.displayText, type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT } }),
450
+ ptv: async (msg, opts) => { const { videoMessage } = await prepareWAMessageMedia({ video: msg.video }, opts); return { ptvMessage: videoMessage } },
451
+ product: async (msg, opts) => { const { imageMessage } = await prepareWAMessageMedia({ image: msg.product.productImage }, opts); return { productMessage: WAProto.Message.ProductMessage.create({ ...msg, product: { ...msg.product, productImage: imageMessage } }) } },
452
+ order: async (msg) => ({ orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: msg.order.id, thumbnail: await normalizeToBuffer(msg.order.thumbnail), itemCount: msg.order.itemCount, status: msg.order.status, surface: msg.order.surface, orderTitle: msg.order.title, message: msg.order.text, sellerJid: msg.order.seller, token: msg.order.token, totalAmount1000: msg.order.amount, totalCurrencyCode: msg.order.currency }) }),
453
+ sections: async (msg) => ({ listMessage: { title: msg.title, buttonText: msg.buttonText, footerText: msg.footer, description: msg.text, sections: msg.sections, listType: proto.Message.ListMessage.ListType.SINGLE_SELECT, contextInfo: { ...(msg.contextInfo || {}), ...(msg.mentions ? { mentionedJid: msg.mentions } : {}) } } }),
454
+ listReply: async (msg) => ({ listResponseMessage: { ...msg.listReply } }),
455
+ event: async (msg, opts) => {
456
+ const startTime = Math.floor(msg.event.startDate.getTime() / 1000)
457
+ const eventMessage = { name: msg.event.name, description: msg.event.description, startTime, isCanceled: msg.event.isCancelled ?? false, extraGuestsAllowed: msg.event.extraGuestsAllowed, isScheduleCall: msg.event.isScheduleCall ?? false, location: msg.event.location, ...(msg.event.endDate ? { endTime: msg.event.endDate.getTime() / 1000 } : {}) }
458
+ if (msg.event.call && opts.getCallLink) eventMessage.joinLink = (msg.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + await opts.getCallLink(msg.event.call, { startTime })
459
+ return { eventMessage, messageContextInfo: { messageSecret: msg.event.messageSecret || randomBytes(32) } }
460
+ },
461
+ poll: async (msg) => {
462
+ msg.poll.selectableCount ||= 0; msg.poll.toAnnouncementGroup ||= false
463
+ if (!Array.isArray(msg.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 })
464
+ if (msg.poll.selectableCount < 0 || msg.poll.selectableCount > msg.poll.values.length) throw new Boom(`poll.selectableCount should be >= 0 and <= ${msg.poll.values.length}`, { statusCode: 400 })
465
+ const pollMsg = { name: msg.poll.name, selectableOptionsCount: msg.poll.selectableCount, options: msg.poll.values.map(optionName => ({ optionName })) }
466
+ const key = msg.poll.toAnnouncementGroup ? 'pollCreationMessageV2' : msg.poll.selectableCount === 1 ? 'pollCreationMessageV3' : 'pollCreationMessage'
467
+ return { [key]: pollMsg, messageContextInfo: { messageSecret: msg.poll.messageSecret || randomBytes(32) } }
468
+ },
469
+ inviteAdmin: async (msg) => ({ newsletterAdminInviteMessage: { inviteExpiration: msg.inviteAdmin.inviteExpiration, caption: msg.inviteAdmin.text, newsletterJid: msg.inviteAdmin.jid, newsletterName: msg.inviteAdmin.subject, jpegThumbnail: await normalizeToBuffer(msg.inviteAdmin.thumbnail) } }),
470
+ requestPayment: async (msg, opts) => {
471
+ const data = msg.requestPayment || msg.payment
472
+ const sticker = data.sticker ? await prepareWAMessageMedia({ sticker: data.sticker }, opts) : null
473
+ const noteMessage = sticker ? { stickerMessage: { ...sticker.stickerMessage, contextInfo: data.contextInfo } } : { extendedTextMessage: { text: data.note || 'Notes', ...(data.contextInfo ? { contextInfo: data.contextInfo } : {}) } }
474
+ const m = { requestPaymentMessage: WAProto.Message.RequestPaymentMessage.fromObject({ expiryTimestamp: data.expiryTimestamp || data.expiry || 0, amount1000: data.amount1000 || data.amount || 0, currencyCodeIso4217: data.currencyCodeIso4217 || data.currency || 'IDR', requestFrom: data.requestFrom || data.from || '0@s.whatsapp.net', noteMessage, background: data.background ?? { id: 'DEFAULT', placeholderArgb: 0xfff0f0f0 } }) }
475
+ if ((data.currencyCodeIso4217 === 'BRL' || data.currency === 'BRL') && data.pixKey) { m.requestPaymentMessage.noteMessage.extendedTextMessage ??= { text: '' }; m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${data.pixKey}` }
476
+ return m
477
+ },
478
+ album: async (msg) => ({ albumMessage: { expectedImageCount: msg.album.filter(i => 'image' in i).length, expectedVideoCount: msg.album.filter(i => 'video' in i).length } }),
479
+ // Proto pass-throughs: one-liners using proto enum values directly
480
+ sharePhoneNumber: async () => ({ protocolMessage: { type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER } }),
481
+ requestPhoneNumber: async () => ({ requestPhoneNumberMessage: {} }),
482
+ limitSharing: async (msg) => ({ protocolMessage: { type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING, limitSharing: { sharingLimited: msg.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } }),
483
+ payment: async (msg, opts) => MESSAGE_HANDLERS.requestPayment(msg, opts),
484
+ }
485
+
486
+ // Interactive overlays: keyed by the input field, each returns a complete replacement for m.
487
+ // Looked up dynamically — adding a new overlay type = one new entry, nothing else.
488
+ const applyContextInfoAndMentions = async (interactive, msg) => {
489
+ if (msg.contextInfo) {
490
+ const ctx = { ...(interactive.contextInfo || {}), ...msg.contextInfo }
491
+ if (ctx.externalAdReply?.thumbnail && Buffer.isBuffer(ctx.externalAdReply.thumbnail) && ctx.externalAdReply.thumbnail.length > 50_000) {
492
+ try {
493
+ ctx.externalAdReply.thumbnail = await sharp(ctx.externalAdReply.thumbnail).resize(200, 200, { fit: 'cover', position: 'center' }).jpeg({ quality: 60 }).toBuffer()
494
+ } catch { }
495
+ }
496
+ interactive.contextInfo = ctx
497
+ }
498
+ if (msg.mentions?.length) interactive.contextInfo = { ...(interactive.contextInfo || {}), mentionedJid: msg.mentions }
499
+ }
500
+
501
+ const OVERLAY_HANDLERS = {
502
+ buttons: async (m, msg) => {
503
+ const hasNativeFlow = msg.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson)
504
+ if (hasNativeFlow) {
505
+ const interactive = { body: { text: msg.text || msg.caption || msg.contentText || '' }, footer: { text: msg.footer || msg.footerText || '' }, nativeFlowMessage: { buttons: msg.buttons.map(b => b.name && b.buttonParamsJson ? b : b.nativeFlowInfo ? { name: b.nativeFlowInfo.name, buttonParamsJson: b.nativeFlowInfo.paramsJson } : { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: b.buttonText?.displayText || b.displayText || '', id: b.buttonId || b.id || '' }) }) } }
506
+ if (msg.title) interactive.header = { title: msg.title, subtitle: msg.subtitle || '', hasMediaAttachment: msg.hasMediaAttachment || false }
507
+ if (Object.keys(m).length) { interactive.header ??= { title: msg.title || '', hasMediaAttachment: true }; Object.assign(interactive.header, m) }
508
+ await applyContextInfoAndMentions(interactive, msg)
509
+ return { interactiveMessage: interactive }
510
+ }
511
+ const bm = { buttons: msg.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) }
512
+ if ('text' in msg) { bm.contentText = msg.text; bm.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY }
513
+ else {
514
+ if ('caption' in msg) bm.contentText = msg.caption
515
+ const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase()
516
+ bm.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY
517
+ Object.assign(bm, m)
518
+ }
519
+ if (msg.title) { bm.text = msg.title; bm.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT }
520
+ if (msg.footer) bm.footerText = msg.footer
521
+ if (msg.contextInfo) bm.contextInfo = { ...(bm.contextInfo || {}), ...msg.contextInfo }
522
+ if (msg.mentions?.length) bm.contextInfo = { ...(bm.contextInfo || {}), mentionedJid: msg.mentions }
523
+ return { buttonsMessage: bm }
524
+ },
525
+ templateButtons: (m, msg) => {
526
+ const ht = { hydratedButtons: msg.templateButtons, ...('text' in msg ? { hydratedContentText: msg.text } : { ...('caption' in msg ? { hydratedContentText: msg.caption } : {}), ...m }) }
527
+ if (msg.footer) ht.hydratedFooterText = msg.footer
528
+ if (msg.contextInfo) ht.contextInfo = { ...(ht.contextInfo || {}), ...msg.contextInfo }
529
+ if (msg.mentions?.length) ht.contextInfo = { ...(ht.contextInfo || {}), mentionedJid: msg.mentions }
530
+ return { templateMessage: { fourRowTemplate: ht, hydratedTemplate: ht } }
531
+ },
532
+ interactiveButtons: async (m, msg) => {
533
+ const interactive = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: msg.interactiveButtons }) }
534
+ if ('text' in msg) { interactive.body = { text: msg.text }; interactive.header = { title: msg.title || '', subtitle: msg.subtitle || '', hasMediaAttachment: false } }
535
+ else if ('caption' in msg) { interactive.body = { text: msg.caption }; interactive.header = { title: msg.title || '', subtitle: msg.subtitle || '', hasMediaAttachment: msg.hasMediaAttachment ?? !!Object.keys(m).length }; if (Object.keys(m).length) Object.assign(interactive.header, m) }
536
+ if (msg.footer) interactive.footer = { text: msg.footer }
537
+ await applyContextInfoAndMentions(interactive, msg)
538
+ return { interactiveMessage: interactive, messageContextInfo: { messageSecret: randomBytes(32) } }
539
+ },
540
+ shop: async (m, msg) => {
541
+ const interactive = { shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({ surface: msg.shop.surface || 1, id: msg.shop.id || msg.id }) }
542
+ if ('text' in msg) interactive.body = { text: msg.text }
543
+ else if ('caption' in msg) interactive.body = { text: msg.caption }
544
+ if (msg.title || Object.keys(m).length) { interactive.header = { title: msg.title || '', subtitle: msg.subtitle || '', hasMediaAttachment: msg.hasMediaAttachment ?? !!Object.keys(m).length }; if (Object.keys(m).length) Object.assign(interactive.header, m) }
545
+ if (msg.footer) interactive.footer = { text: msg.footer }
546
+ await applyContextInfoAndMentions(interactive, msg)
547
+ return { interactiveMessage: interactive }
548
+ },
549
+ collection: async (m, msg) => {
550
+ const interactive = { collectionMessage: { bizJid: msg.collection.bizJid, id: msg.collection.id, messageVersion: msg.collection.version } }
551
+ if ('text' in msg) { interactive.body = { text: msg.text }; interactive.header = { title: msg.title || '', hasMediaAttachment: false } }
552
+ else if ('caption' in msg) { interactive.body = { text: msg.caption }; interactive.header = { title: msg.title || '', hasMediaAttachment: msg.hasMediaAttachment ?? false }; if (Object.keys(m).length) Object.assign(interactive.header, m) }
553
+ if (msg.footer) interactive.footer = { text: msg.footer }
554
+ await applyContextInfoAndMentions(interactive, msg)
555
+ return { interactiveMessage: interactive }
556
+ },
557
+ }
558
+
559
+ export const generateWAMessageContent = async (message, options = {}) => {
560
+ if (!message || typeof message !== 'object') return WAProto.Message.create({})
561
+ const msgKeys = Object.keys(message)
562
+ // Raw proto pass-through: no high-level keys and already has proto field names, or is a known wrapper
563
+ const isWrapper = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message)
564
+ if (isWrapper || (!msgKeys.some(k => HIGH_LEVEL_KEYS.has(k)) && msgKeys.some(k => k.endsWith('Message') || k === 'conversation'))) return WAProto.Message.create(message)
565
+
566
+ let m = {}
567
+ // Text without interactive overlay keys goes straight to text handler
568
+ const overlayKeys = Object.keys(OVERLAY_HANDLERS)
569
+ if ('text' in message && !overlayKeys.some(k => k in message)) {
570
+ m = await MESSAGE_HANDLERS.text(message, options)
571
+ } else {
572
+ // Find first matching handler key (skip 'text' here — handled above)
573
+ const handlerKey = msgKeys.find(k => k !== 'text' && MESSAGE_HANDLERS[k])
574
+ if (handlerKey) {
575
+ const result = await MESSAGE_HANDLERS[handlerKey](message, options)
576
+ if (result?._batched) return { stickerPackMessage: result.stickerPackMessage, isBatched: true, batchCount: result.batchCount }
577
+ if (result?._finalize) return WAProto.Message.create({ stickerPackMessage: result.stickerPackMessage })
578
+ m = result
579
+ } else if (MEDIA_KEYS.some(k => k in message)) {
580
+ m = await prepareWAMessageMedia(message, options)
581
+ }
582
+ }
583
+
584
+ // Apply overlays: any overlay key present in message replaces m
585
+ for (const key of overlayKeys) { if (key in message && message[key]) { m = OVERLAY_HANDLERS[key](m, message); break } }
586
+
587
+ // Merge contextInfo / mentions onto the primary content key — deep-merges externalAdReply to prevent thumbnail/field loss
588
+ const finalKey = Object.keys(m)[0]
589
+ if ((message.contextInfo || message.mentions?.length) && finalKey && m[finalKey] && typeof m[finalKey] === 'object') {
590
+ const existing = m[finalKey].contextInfo || {}
591
+ const incoming = message.contextInfo || {}
592
+ m[finalKey].contextInfo = { ...existing, ...incoming, ...(existing.externalAdReply || incoming.externalAdReply ? { externalAdReply: { ...(existing.externalAdReply || {}), ...(incoming.externalAdReply || {}) } } : {}), ...(message.mentions?.length ? { mentionedJid: message.mentions } : {}) }
593
+ }
594
+
595
+ if ('viewOnce' in message && message.viewOnce || 'viewOnceMessage' in message) m = { viewOnceMessage: { message: m } }
596
+ if ('edit' in message) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } }
597
+
598
+ if (shouldIncludeReportingToken(m)) { m.messageContextInfo ??= {}; m.messageContextInfo.messageSecret ??= randomBytes(32) }
599
+ return WAProto.Message.create(m)
600
+ }
601
+
602
+ // ─── ASSEMBLE FULL WA MESSAGE ─────────────────────────────────────────────────
603
+
604
+ export const generateWAMessageFromContent = (jid, message, options) => {
605
+ if (!options.timestamp) options.timestamp = new Date()
606
+ const innerMessage = normalizeMessageContent(message)
607
+ const key = getContentType(innerMessage)
608
+ const { quoted, userJid } = options
609
+ if (quoted && !isJidNewsletter(jid)) {
610
+ const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
611
+ const normalizedQuoted = normalizeMessageContent(quoted.message)
612
+ if (normalizedQuoted) {
613
+ const quotedType = getContentType(normalizedQuoted)
614
+ const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
615
+ const quotedContent = quotedMsg[quotedType]
616
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
617
+ const contextInfo = innerMessage[key]?.contextInfo || {}
618
+ contextInfo.participant = jidNormalizedUser(participant)
619
+ contextInfo.stanzaId = quoted.key.id
620
+ contextInfo.quotedMessage = quotedMsg
621
+ if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
622
+ if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
623
+ }
624
+ }
625
+ if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
626
+ innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
627
+ }
628
+ return WAProto.WebMessageInfo.fromObject({
629
+ key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
630
+ message: WAProto.Message.create(message),
631
+ messageTimestamp: unixTimestampSeconds(options.timestamp),
632
+ messageStubParameters: [],
633
+ participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
634
+ status: WAMessageStatus.PENDING,
635
+ })
636
+ }
637
+
638
+ // Primary entry point. generateWAMessageContent returns a proto Message object.
639
+ // generateWAMessageFromContent wraps existing content into a WebMessageInfo.
640
+ // Call the wrong one by accident and it self-corrects rather than failing silently.
641
+ export const generateWAMessage = async (jid, content, options = {}) => {
642
+ if (content?.key?.remoteJid && content?.message) return content
643
+ options.logger = options?.logger?.child({ msgId: options.messageId })
644
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
645
+ }
646
+
647
+ // ─── UPDATE UTILITIES ─────────────────────────────────────────────────────────
648
+
649
+ export const updateMessageWithReceipt = (msg, receipt) => {
650
+ msg.userReceipt ||= []
651
+ const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid)
652
+ if (recp) Object.assign(recp, receipt)
653
+ else msg.userReceipt.push(receipt)
654
+ }
655
+
656
+ export const updateMessageWithReaction = (msg, reaction) => {
657
+ const authorID = getKeyAuthor(reaction.key)
658
+ msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID)
659
+ reaction.text ||= ''
660
+ msg.reactions.push(reaction)
661
+ }
662
+
663
+ export const updateMessageWithPollUpdate = (msg, update) => {
664
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey)
665
+ msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
666
+ if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update)
667
+ }
668
+
669
+ export const updateMessageWithEventResponse = (msg, update) => {
670
+ const authorID = getKeyAuthor(update.eventResponseMessageKey)
671
+ msg.eventResponses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID)
672
+ msg.eventResponses.push(update)
673
+ }
674
+
675
+ export const getAggregateVotesInPollMessage = ({ message, pollUpdates }, meId) => {
676
+ const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
677
+ const voteHashMap = opts.reduce((acc, opt) => { acc[sha256(Buffer.from(opt.optionName || '')).toString()] = { name: opt.optionName || '', voters: [] }; return acc }, {})
678
+ for (const update of pollUpdates || []) {
679
+ if (!update.vote) continue
680
+ for (const option of update.vote.selectedOptions || []) {
681
+ const hash = option.toString()
682
+ voteHashMap[hash] ||= { name: 'Unknown', voters: [] }
683
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId))
684
+ }
685
+ }
686
+ return Object.values(voteHashMap)
687
+ }
688
+
689
+ export const getAggregateResponsesInEventMessage = ({ eventResponses }, meId) => {
690
+ const responseMap = { GOING: { response: 'GOING', responders: [] }, NOT_GOING: { response: 'NOT_GOING', responders: [] }, MAYBE: { response: 'MAYBE', responders: [] } }
691
+ for (const update of eventResponses || []) { const type = update.eventResponse || 'UNKNOWN'; if (responseMap[type]) responseMap[type].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId)) }
692
+ return Object.values(responseMap)
693
+ }
694
+
695
+ export const aggregateMessageKeysNotFromMe = (keys) => {
696
+ const keyMap = {}
697
+ for (const { remoteJid, id, participant, fromMe } of keys) {
698
+ if (!fromMe) { const uqKey = `${remoteJid}:${participant || ''}`; keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] }; keyMap[uqKey].messageIds.push(id) }
699
+ }
700
+ return Object.values(keyMap)
701
+ }
702
+
703
+ export const downloadMediaMessage = async (message, type, options, ctx) => {
704
+ const downloadMsg = async () => {
705
+ let normalized = message
706
+ if (!message.message && message.key) normalized = { key: message.key, message: message.quoted?.message || message, messageTimestamp: message.messageTimestamp }
707
+ const mContent = extractMessageContent(normalized.message)
708
+ if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message })
709
+ const contentType = getContentType(mContent)
710
+ let mediaType = contentType?.replace('Message', '')
711
+ const media = mContent[contentType]
712
+ if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) throw new Boom(`"${contentType}" message is not a media message`)
713
+ const download = ('thumbnailDirectPath' in media && !('url' in media)) ? { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey } : media
714
+ if ('thumbnailDirectPath' in media && !('url' in media)) mediaType = 'thumbnail-link'
715
+ const stream = await downloadContentFromMessage(download, mediaType, options)
716
+ if (type === 'buffer') { const chunks = []; for await (const chunk of stream) chunks.push(chunk); return Buffer.concat(chunks) }
717
+ return stream
718
+ }
719
+ return downloadMsg().catch(async (error) => {
720
+ if (ctx && typeof error?.status === 'number' && REUPLOAD_REQUIRED_STATUS.includes(error.status)) { ctx.logger.info({ key: message.key }, 'sending reupload media request...'); message = await ctx.reuploadRequest(message); return downloadMsg() }
721
+ throw error
722
+ })
723
+ }
724
+
725
+ export const assertMediaContent = (content) => {
726
+ content = extractMessageContent(content)
727
+ const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage || content?.stickerPackMessage
728
+ if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content })
729
+ return mediaContent
730
+ }
731
+
732
+ 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'
733
+
734
+ export const patchMessageForMdIfRequired = (message) => {
735
+ if (message?.buttonsMessage || message?.templateMessage || message?.listMessage || message?.interactiveMessage?.nativeFlowMessage) {
736
+ message = { viewOnceMessageV2Extension: { message: { messageContextInfo: { deviceListMetadataVersion: 2, deviceListMetadata: {} }, ...message } } }
737
+ }
738
+ return message
739
+ }
740
+
741
+ export const hasNonNullishProperty = (message, key) => typeof message === 'object' && message !== null && key in message && message[key] !== null && message[key] !== undefined
742
+ export const hasOptionalProperty = (obj, key) => typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null && obj[key] !== undefined