@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.
- package/LICENSE +21 -0
- package/README.md +3203 -2
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +590 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +448 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +904 -0
- package/lib/Socket/messages-send.js +693 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +752 -0
- package/lib/Socket/nexus-handler.js +55 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +798 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +316 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +39 -0
- package/lib/Utils/key-store.js +3 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +742 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +142 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +102 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +68 -0
- 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
|