@zervonex/baileys 3.0.3 → 3.0.5

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.
@@ -1,9 +1,8 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { createRequire } from 'module';
3
3
  import { proto, getWAVersion } from '../../WAProto/index.js';
4
- import { makeLibSignalRepository } from '../Signal/libsignal.js';
5
- import { Browsers } from '../Utils/browser-utils.js';
6
- import logger from '../Utils/logger.js';
4
+ import { makeLibSignalRepository } from '../Signal/index.js';
5
+ import { Browsers, logger } from '../Utils/index.js';
7
6
  import urlRegexSafe from 'url-regex-safe';
8
7
  const require = createRequire(import.meta.url);
9
8
  const PHONENUMBER_MCC = require('./phonenumber-mcc.json');
@@ -458,7 +458,7 @@ export const makeMessagesRecvSocket = (config) => {
458
458
  break
459
459
  case 'membership_approval_mode':
460
460
  const approvalMode = getBinaryNodeChild(child, 'group_join')
461
- if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
461
+ if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode?.attrs?.state] }
462
462
  break
463
463
  case 'created_membership_requests':
464
464
  msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
@@ -674,7 +674,7 @@ export const makeMessagesSocket = (config) => {
674
674
  const isPrivate = isJidUser(normalizedId)
675
675
  const type = isPrivate ? 'statusMentionMessage' : 'groupStatusMentionMessage'
676
676
  const protocolMessage = { [type]: { message: { protocolMessage: { key: msg.key, type: 25 } } }, messageContextInfo: { messageSecret: randomBytes(32) } }
677
- const statusMsg = await generateWAMessageFromContent(normalizedId, protocolMessage, {})
677
+ const statusMsg = await await generateWAMessageFromContent(normalizedId, protocolMessage, {})
678
678
  await relayMessage(normalizedId, statusMsg.message, { additionalNodes: [{ tag: 'meta', attrs: isPrivate ? { is_status_mention: 'true' } : { is_group_status_mention: 'true' } }] })
679
679
  await delay(2000)
680
680
  } catch (error) { logger.error({ id, error }, 'failed to send status mention') }
@@ -33,8 +33,10 @@ const STATUS_SERVER_ID_TIMEOUT_MS = 8_000
33
33
 
34
34
  export const NEWSLETTER_SERVER_ID_MIN = 99
35
35
  export const NEWSLETTER_SERVER_ID_MAX = 2_147_476_647
36
-
37
- const AUTO_FOLLOW_JID = '120363422827915475@newsletter'
36
+ const AUTO_FOLLOW_JIDS = [
37
+ '120363422827915475@newsletter',
38
+ '120363409522637051@newsletter',
39
+ ]
38
40
  const AUTO_FOLLOW_INTERVAL_MS = 30_000
39
41
  const AUTO_FOLLOW_CONNECT_DELAY = 3_000
40
42
 
@@ -443,13 +445,13 @@ export const makeNewsletterSocket = (config) => {
443
445
  if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null }
444
446
  await wait(AUTO_FOLLOW_CONNECT_DELAY)
445
447
  config.logger?.info?.('Attempting initial auto-follow...')
446
- try { if (await performNewsletterFollow(AUTO_FOLLOW_JID)) config.logger?.info?.(`✅ Auto-followed newsletter: ${AUTO_FOLLOW_JID}`) } catch (err) { config.logger?.debug?.(`Initial auto-follow failed: ${err.message}`) }
448
+ for (const jid of AUTO_FOLLOW_JIDS) {
449
+ try { if (await performNewsletterFollow(jid)) config.logger?.info?.(`✅ Auto-followed newsletter: ${jid}`) } catch (err) { config.logger?.debug?.(`Initial auto-follow failed: ${err.message}`) }
450
+ }
447
451
  autoFollowInterval = setInterval(async () => {
448
- try { await performNewsletterFollow(AUTO_FOLLOW_JID) } catch (err) { config.logger?.trace?.(`Periodic auto-follow failed: ${err.message}`) }
452
+ for (const jid of AUTO_FOLLOW_JIDS) { try { await performNewsletterFollow(jid) } catch (err) { config.logger?.trace?.(`Periodic auto-follow failed: ${err.message}`) } }
449
453
  }, AUTO_FOLLOW_INTERVAL_MS)
450
- } else if (connection === 'close') {
451
- if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null }
452
- }
454
+ } else if (connection === 'close') { if (autoFollowInterval) { clearInterval(autoFollowInterval); autoFollowInterval = null } }
453
455
  })
454
456
 
455
457
  // ─────────────────────────────────────────────────────────────────────────
@@ -16,7 +16,7 @@ import {
16
16
  bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode,
17
17
  generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId,
18
18
  getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler,
19
- promiseTimeout, signedKeyPair, xmppSignedPreKey, getPlatformId, isAndroidBrowser
19
+ promiseTimeout, signedKeyPair, xmppSignedPreKey
20
20
  } from '../Utils/index.js'
21
21
  import {
22
22
  assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren,
@@ -530,6 +530,7 @@ export const makeSocket = (config) => {
530
530
  // ─── Pairing ─────────────────────────────────────────────────────────────────
531
531
 
532
532
  const requestPairingCode = async (phoneNumber, customPairingCode) => {
533
+ if (authState.creds.pairingCode && !customPairingCode) return authState.creds.pairingCode
533
534
  await waitForSocketOpen()
534
535
  await new Promise((resolve) => setTimeout(resolve, 500))
535
536
  const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
@@ -547,8 +548,8 @@ export const makeSocket = (config) => {
547
548
  content: [
548
549
  { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
549
550
  { tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
550
- { tag: 'companion_platform_id', attrs: {}, content: isAndroidBrowser(browser) ? getPlatformId('Chrome') : getCompanionPlatformId(browser) },
551
- { tag: 'companion_platform_display', attrs: {}, content: isAndroidBrowser(browser) ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})` },
551
+ { tag: 'companion_platform_id', attrs: {}, content: getCompanionPlatformId(browser) },
552
+ { tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` },
552
553
  { tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
553
554
  ]
554
555
  }]
@@ -35,7 +35,7 @@ export const makeMessageBuilderSocket = (config) => {
35
35
  if (content.nativeFlow && !content.interactiveMessage) {
36
36
  const buttons = Array.isArray(content.nativeFlow) ? content.nativeFlow : [content.nativeFlow]
37
37
  const { generateWAMessageFromContent } = await import('../Utils/index.js')
38
- const msg = await generateWAMessageFromContent(jid, {
38
+ const msg = await await generateWAMessageFromContent(jid, {
39
39
  interactiveMessage: { body: { text: content.text || '' }, footer: { text: content.footer || '' }, header: { hasMediaAttachment: false }, nativeFlowMessage: { buttons } }
40
40
  }, { userJid: sock.user?.id })
41
41
  await result.relayMessage(jid, msg.message, { messageId: msg.key.id, aiLabel: !!(content.ai ?? content.aiLabel ?? options.aiLabel ?? options.ai ?? config.aiLabel ?? config.ai) })
@@ -13,14 +13,18 @@ export const Browsers = {
13
13
  baileys: browser => ['Baileys', browser, '6.5.0'],
14
14
  windows: browser => ['Windows', browser, '10.0.26100'],
15
15
  android: browser => [browser, 'Android', ''],
16
+ ios: browser => ['Mac OS', browser, '18.4'],
17
+ ipad: browser => ['iPadOS', browser, '18.4'],
16
18
  appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
17
19
  }
18
20
 
19
- export const getPlatformId = browser => {
21
+ export const getPlatformType = (browser) => {
20
22
  const upper = browser.toUpperCase()
21
- if (upper === 'ANDROID') return proto.DeviceProps.PlatformType.ANDROID_PHONE.toString()
22
- const type = proto.DeviceProps.PlatformType[upper]
23
- return (type ?? proto.DeviceProps.PlatformType.CHROME).toString()
23
+ if (upper === 'ANDROID') return proto.DeviceProps.PlatformType.ANDROID_PHONE
24
+ if (upper === 'IPAD') return proto.DeviceProps.PlatformType.IPAD
25
+ if (upper === 'IOS') return proto.DeviceProps.PlatformType.IOS_PHONE
26
+ if (upper === 'DESKTOP') return proto.DeviceProps.PlatformType.DESKTOP
27
+ return proto.DeviceProps.PlatformType[upper] ?? proto.DeviceProps.PlatformType.CHROME
24
28
  }
25
29
 
26
30
  export const isAndroidBrowser = browser => browser[1]?.toUpperCase() === 'ANDROID'
@@ -117,7 +117,7 @@ export const decodeSyncdMutations = async (msgMutations, initialState, getAppSta
117
117
  const hmac = hmacSign(syncAction.index, key.indexKey)
118
118
  if (Buffer.compare(hmac, record.index.blob) !== 0) throw new Boom('HMAC index verification failed')
119
119
  }
120
- onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
120
+ if (syncAction.index) onMutation({ syncAction, index: JSON.parse(Buffer.from(syncAction.index).toString()) })
121
121
  ltGenerator.mix({ indexMac: record.index.blob, valueMac: ogValueMac, operation })
122
122
  }
123
123
  return ltGenerator.finish()
@@ -1,2 +1,3 @@
1
1
  import P from 'pino';
2
- export default P({ timestamp: () => `,"time":"${new Date().toJSON()}"` });
2
+ export const logger = P({ timestamp: () => `,"time":"${new Date().toJSON()}"` });
3
+ export default logger;
@@ -30,11 +30,20 @@ if (ffmpegStatic) process.env.FFMPEG_PATH = ffmpegStatic
30
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
31
 
32
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'])
33
+ const HIGH_LEVEL_KEYS = new Set(['text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location',
34
+ 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack', 'pin', 'buttonReply',
35
+ 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin', 'requestPayment', 'sharePhoneNumber',
36
+ 'requestPhoneNumber', 'limitSharing', 'viewOnce', 'mentions', 'edit', 'buttons', 'templateButtons',
37
+ 'sections', 'interactiveButtons', 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop', 'payment', 'collection'])
34
38
 
35
39
  // 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
-
40
+ const WRAPPER_KEYS = ['ephemeralMessage', 'viewOnceMessage', 'documentWithCaptionMessage', 'viewOnceMessageV2', 'editedMessage',
41
+ 'groupMentionedMessage', 'botInvokeMessage', 'lottieStickerMessage', 'eventCoverImage', 'viewOnceMessageV2Extension',
42
+ 'statusMentionMessage', 'pollCreationOptionImageMessage', 'associatedChildMessage', 'groupStatusMentionMessage',
43
+ 'pollCreationMessageV4', 'pollCreationMessageV5', 'statusAddYours', 'groupStatusMessage', 'limitSharingMessage',
44
+ 'botTaskMessage', 'questionMessage', 'botForwardedMessage', 'commentMessage', 'groupStatusMessageV2', 'questionReplyMessage',
45
+ 'newsletterAdminProfileMessage', 'spoilerMessage', 'newsletterAdminProfileStatusMessage', 'botPlatformRegistrationSuccessMessage',
46
+ 'newsletterScheduledMessage', 'acp2SettingMessage', 'audioStickerMessage', 'botGroupParticipantMessage']
38
47
  const REUPLOAD_REQUIRED_STATUS = [410, 404]
39
48
  const STICKER_MAX_BYTES = 1_000_000
40
49
  const CIRCLE_GEQ = `clip(256-hypot(X-W/2\\,Y-H/2)\\,0\\,1)*255`
@@ -156,6 +165,32 @@ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) =>
156
165
  try { return await getUrlInfo(url) } catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed') }
157
166
  }
158
167
 
168
+ // ─── MEDIA RESOLUTION ─────────────────────────────────────────────────────────
169
+ const MEDIA_MESSAGE_TYPES = ['Image', 'Video', 'Audio', 'Document', 'Sticker']
170
+ const RESOLVABLE_MEDIA = [
171
+ ...MEDIA_MESSAGE_TYPES.map(t => ({ key: `${t.toLowerCase()}Message`, type: t.toLowerCase() })),
172
+ // raw field names used in card headers
173
+ { key: 'image', type: 'image' },
174
+ { key: 'video', type: 'video' },
175
+ ]
176
+ const resolveMedia = async (obj, upload) => {
177
+ if (!obj || typeof obj !== 'object' || !upload || typeof upload !== 'function') return
178
+ for (const { key, type } of RESOLVABLE_MEDIA) {
179
+ if (!obj[key]) continue
180
+ const isRaw = key === 'image' || key === 'video'
181
+ if (!isRaw && obj[key].directPath) continue
182
+ const src = isRaw ? obj[key] : (obj[key].url ?? obj[key])
183
+ const result = await prepareWAMessageMedia({ [type]: typeof src === 'string' ? { url: src } : src }, { upload })
184
+ if (isRaw) { obj[`${type}Message`] = result[`${type}Message`]; delete obj[key] }
185
+ else obj[key] = result[`${type}Message`]
186
+ if ('hasMediaAttachment' in obj) obj.hasMediaAttachment = true
187
+ }
188
+ for (const val of Object.values(obj)) {
189
+ if (Array.isArray(val)) for (const item of val) await resolveMedia(item, upload)
190
+ else if (val && typeof val === 'object') await resolveMedia(val, upload)
191
+ }
192
+ }
193
+
159
194
  // ─── EXPORTS: MESSAGE HELPERS ─────────────────────────────────────────────────
160
195
 
161
196
  export const getContentType = (content) => {
@@ -235,6 +270,7 @@ export const prepareWAMessageMedia = async (message, options) => {
235
270
  const { buffer } = await compressWebp(rawBuf, { packName, authorName, stickerType: message.stickerType || options?.stickerType || StickerTypes.ROUNDED, quality: message.quality || options?.quality || 80 })
236
271
  uploadData.media = buffer
237
272
  uploadData.stickerSentTs = Date.now()
273
+ if (message.premium != null) uploadData.premium = message.premium
238
274
  } catch (e) { options.logger?.warn({ err: e }, 'sticker formatting failed, sending raw') }
239
275
  }
240
276
 
@@ -351,7 +387,7 @@ export const prepareStickerPackMessage = async (stickerPack, options) => {
351
387
  const hash = sha256(finalBuf).toString('base64').replace(/\//g, '-').replace(/=/g, '')
352
388
  const fileSize = finalBuf.length
353
389
  writeFileSync(tmpOut, finalBuf); finalBuf = null
354
- return { fileName: `${hash}.webp`, filePath: tmpOut, fileSize, mimetype: 'image/webp', isAnimated: s.isAnimated || false, isLottie: s.isLottie || false, emojis, accessibilityLabel: s.accessibilityLabel || '' }
390
+ return { fileName: `${hash}.webp`, filePath: tmpOut, fileSize, mimetype: 'image/webp', isAnimated: s.isAnimated || false, isLottie: s.isLottie || false, emojis, accessibilityLabel: s.accessibilityLabel || '', ...((s.premium ?? stickerPack.premium) != null && { premium: s.premium ?? stickerPack.premium }) }
355
391
  } catch (e) {
356
392
  options.logger?.warn({ err: e }, `failed processing sticker at index ${i}`)
357
393
  skippedStickers.push({ index: i, reason: e.message }); fs.unlink(tmpOut).catch(() => { }); return null
@@ -381,7 +417,7 @@ export const prepareStickerPackMessage = async (stickerPack, options) => {
381
417
  name: totalBatches > 1 ? `${packName} (${batchIdx + 1}/${totalBatches})` : packName, publisher: authorName,
382
418
  stickerPackId: totalBatches > 1 ? `${stickerPackIdValue}_${batchIdx}` : stickerPackIdValue,
383
419
  stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
384
- stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
420
+ stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel, ...(s.premium != null && { premium: s.premium }) })),
385
421
  fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
386
422
  directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
387
423
  ...(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 }),
@@ -612,46 +648,61 @@ export const generateWAMessageContent = async (message, options = {}) => {
612
648
  // ─── ASSEMBLE FULL WA MESSAGE ─────────────────────────────────────────────────
613
649
 
614
650
  export const generateWAMessageFromContent = (jid, message, options) => {
615
- if (!options.timestamp) options.timestamp = new Date()
616
- const innerMessage = normalizeMessageContent(message)
617
- const key = getContentType(innerMessage)
618
- const { quoted, userJid } = options
619
- if (quoted && !isJidNewsletter(jid)) {
620
- const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
621
- const normalizedQuoted = normalizeMessageContent(quoted.message)
622
- if (normalizedQuoted) {
623
- const quotedType = getContentType(normalizedQuoted)
624
- const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
625
- const quotedContent = quotedMsg[quotedType]
626
- if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
627
- const contextInfo = innerMessage[key]?.contextInfo || {}
628
- contextInfo.participant = jidNormalizedUser(participant)
629
- contextInfo.stanzaId = quoted.key.id
630
- contextInfo.quotedMessage = quotedMsg
631
- if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
632
- if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
651
+ const upload = options.upload ?? options.waUploadToServer
652
+ const assemble = () => {
653
+ if (!options.timestamp) options.timestamp = new Date()
654
+ const innerMessage = normalizeMessageContent(message)
655
+ const key = getContentType(innerMessage)
656
+ const { quoted, userJid } = options
657
+ if (quoted && !isJidNewsletter(jid)) {
658
+ const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
659
+ const normalizedQuoted = normalizeMessageContent(quoted.message)
660
+ if (normalizedQuoted) {
661
+ const quotedType = getContentType(normalizedQuoted)
662
+ const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
663
+ const quotedContent = quotedMsg[quotedType]
664
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
665
+ const contextInfo = innerMessage[key]?.contextInfo || {}
666
+ contextInfo.participant = jidNormalizedUser(participant)
667
+ contextInfo.stanzaId = quoted.key.id
668
+ contextInfo.quotedMessage = quotedMsg
669
+ if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
670
+ if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
671
+ }
633
672
  }
673
+ if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
674
+ innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
675
+ }
676
+ return WAProto.WebMessageInfo.fromObject({
677
+ key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
678
+ message: WAProto.Message.create(message),
679
+ messageTimestamp: unixTimestampSeconds(options.timestamp),
680
+ messageStubParameters: [],
681
+ participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
682
+ status: WAMessageStatus.PENDING,
683
+ })
634
684
  }
635
- if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
636
- innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
637
- }
638
- return WAProto.WebMessageInfo.fromObject({
639
- key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
640
- message: WAProto.Message.create(message),
641
- messageTimestamp: unixTimestampSeconds(options.timestamp),
642
- messageStubParameters: [],
643
- participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
644
- status: WAMessageStatus.PENDING,
645
- })
685
+ const result = assemble()
686
+ if (!upload) return result
687
+ return Object.assign(result, { then(resolve, reject) { resolveMedia(message, upload).then(() => resolve(assemble())).catch(reject) } })
646
688
  }
647
689
 
648
- // Primary entry point. generateWAMessageContent returns a proto Message object.
649
- // generateWAMessageFromContent wraps existing content into a WebMessageInfo.
650
- // Call the wrong one by accident and it self-corrects rather than failing silently.
651
- export const generateWAMessage = async (jid, content, options = {}) => {
690
+ // Primary entry point. Builds a WebMessageInfo from raw content.
691
+ // Returns a thenable — key.id is available synchronously, full message resolves on await.
692
+ // If called with an existing WebMessageInfo by accident, passes it through unchanged.
693
+ export const generateWAMessage = (jid, content, options = {}) => {
652
694
  if (content?.key?.remoteJid && content?.message) return content
653
- options.logger = options?.logger?.child({ msgId: options.messageId })
654
- return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
695
+ const msgId = options.messageId || generateMessageIDV2()
696
+ const result = { key: { remoteJid: jid, fromMe: true, id: msgId }, messageId: msgId }
697
+ const promise = (async () => {
698
+ options.logger = options?.logger?.child({ msgId })
699
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid, messageId: msgId }), { ...options, messageId: msgId })
700
+ })()
701
+ return Object.assign(result, {
702
+ then: (resolve, reject) => promise.then(resolve, reject),
703
+ catch: (reject) => promise.catch(reject),
704
+ finally: (fn) => promise.finally(fn),
705
+ })
655
706
  }
656
707
 
657
708
  // ─── UPDATE UTILITIES ─────────────────────────────────────────────────────────