@nexustechpro/baileys 2.0.1 → 2.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.
- package/LICENSE +1 -1
- package/README.md +924 -1299
- package/lib/Defaults/baileys-version.json +6 -2
- package/lib/Defaults/index.js +172 -172
- package/lib/Signal/libsignal.js +380 -292
- package/lib/Signal/lid-mapping.js +264 -171
- package/lib/Socket/Client/index.js +2 -2
- package/lib/Socket/Client/types.js +10 -10
- package/lib/Socket/Client/websocket.js +45 -310
- package/lib/Socket/business.js +375 -375
- package/lib/Socket/chats.js +909 -963
- package/lib/Socket/communities.js +430 -430
- package/lib/Socket/groups.js +342 -342
- package/lib/Socket/index.js +22 -22
- package/lib/Socket/messages-recv.js +777 -743
- package/lib/Socket/messages-send.js +667 -393
- package/lib/Socket/mex.js +50 -50
- package/lib/Socket/newsletter.js +148 -148
- package/lib/Socket/nexus-handler.js +75 -261
- package/lib/Socket/socket.js +709 -1201
- package/lib/Store/index.js +5 -5
- package/lib/Store/make-cache-manager-store.js +81 -81
- package/lib/Store/make-in-memory-store.js +416 -416
- package/lib/Store/make-ordered-dictionary.js +81 -81
- package/lib/Store/object-repository.js +30 -30
- package/lib/Types/Auth.js +1 -1
- package/lib/Types/Bussines.js +1 -1
- package/lib/Types/Call.js +1 -1
- package/lib/Types/Chat.js +7 -7
- package/lib/Types/Contact.js +1 -1
- package/lib/Types/Events.js +1 -1
- package/lib/Types/GroupMetadata.js +1 -1
- package/lib/Types/Label.js +24 -24
- package/lib/Types/LabelAssociation.js +6 -6
- package/lib/Types/Message.js +10 -10
- package/lib/Types/Newsletter.js +28 -28
- package/lib/Types/Product.js +1 -1
- package/lib/Types/Signal.js +1 -1
- package/lib/Types/Socket.js +2 -2
- package/lib/Types/State.js +12 -12
- package/lib/Types/USync.js +1 -1
- package/lib/Types/index.js +25 -25
- package/lib/Utils/auth-utils.js +264 -256
- package/lib/Utils/baileys-event-stream.js +55 -55
- package/lib/Utils/browser-utils.js +27 -27
- package/lib/Utils/business.js +228 -230
- package/lib/Utils/chat-utils.js +694 -764
- package/lib/Utils/crypto.js +109 -135
- package/lib/Utils/decode-wa-message.js +310 -314
- package/lib/Utils/event-buffer.js +547 -547
- package/lib/Utils/generics.js +297 -297
- package/lib/Utils/history.js +91 -83
- package/lib/Utils/index.js +21 -20
- package/lib/Utils/key-store.js +17 -0
- package/lib/Utils/link-preview.js +97 -88
- package/lib/Utils/logger.js +2 -2
- package/lib/Utils/lt-hash.js +47 -47
- package/lib/Utils/make-mutex.js +39 -39
- package/lib/Utils/message-retry-manager.js +148 -148
- package/lib/Utils/messages-media.js +534 -532
- package/lib/Utils/messages.js +705 -705
- package/lib/Utils/noise-handler.js +255 -255
- package/lib/Utils/pre-key-manager.js +105 -105
- package/lib/Utils/process-message.js +412 -412
- package/lib/Utils/signal.js +160 -158
- package/lib/Utils/use-multi-file-auth-state.js +120 -120
- package/lib/Utils/validate-connection.js +194 -194
- package/lib/WABinary/constants.js +1300 -1300
- package/lib/WABinary/decode.js +237 -237
- package/lib/WABinary/encode.js +232 -232
- package/lib/WABinary/generic-utils.js +252 -211
- package/lib/WABinary/index.js +5 -5
- package/lib/WABinary/jid-utils.js +279 -95
- package/lib/WABinary/types.js +1 -1
- package/lib/WAM/BinaryInfo.js +9 -9
- package/lib/WAM/constants.js +22852 -22852
- package/lib/WAM/encode.js +149 -149
- package/lib/WAM/index.js +3 -3
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
- package/lib/WAUSync/Protocols/index.js +4 -4
- package/lib/WAUSync/USyncQuery.js +93 -93
- package/lib/WAUSync/USyncUser.js +22 -22
- package/lib/WAUSync/index.js +3 -3
- package/lib/index.js +66 -66
- package/package.json +171 -144
- package/lib/Signal/Group/ciphertext-message.js +0 -12
- package/lib/Signal/Group/group-session-builder.js +0 -30
- package/lib/Signal/Group/group_cipher.js +0 -100
- package/lib/Signal/Group/index.js +0 -12
- package/lib/Signal/Group/keyhelper.js +0 -18
- package/lib/Signal/Group/sender-chain-key.js +0 -26
- package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
- package/lib/Signal/Group/sender-key-message.js +0 -66
- package/lib/Signal/Group/sender-key-name.js +0 -48
- package/lib/Signal/Group/sender-key-record.js +0 -41
- package/lib/Signal/Group/sender-key-state.js +0 -84
- package/lib/Signal/Group/sender-message-key.js +0 -26
package/lib/Utils/messages.js
CHANGED
|
@@ -1,706 +1,706 @@
|
|
|
1
|
-
import { Boom } from '@hapi/boom';
|
|
2
|
-
import { randomBytes } from 'crypto';
|
|
3
|
-
import { promises as fs } from 'fs';
|
|
4
|
-
import { zip } from 'fflate';
|
|
5
|
-
import { proto } from '../../WAProto/index.js';
|
|
6
|
-
import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
|
|
7
|
-
import { WAMessageStatus, WAProto } from '../Types/index.js';
|
|
8
|
-
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
|
-
import { sha256 } from './crypto.js';
|
|
10
|
-
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
|
|
11
|
-
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
|
|
12
|
-
|
|
13
|
-
const MIMETYPE_MAP = { image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf', audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg' };
|
|
14
|
-
const MessageTypeProto = { image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage, audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage, document: WAProto.Message.DocumentMessage };
|
|
15
|
-
|
|
16
|
-
// High-level content keys that need processing (not raw WAProto)
|
|
17
|
-
const HIGH_LEVEL_KEYS = ['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'];
|
|
18
|
-
|
|
19
|
-
// ===== UTILITIES =====
|
|
20
|
-
export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
|
|
21
|
-
|
|
22
|
-
export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
|
|
23
|
-
const url = extractUrlFromText(text);
|
|
24
|
-
if (!getUrlInfo || !url) return;
|
|
25
|
-
try { return await getUrlInfo(url); }
|
|
26
|
-
catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed'); }
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const assertColor = (color) => {
|
|
30
|
-
if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1;
|
|
31
|
-
let hex = color.trim().replace('#', '');
|
|
32
|
-
return parseInt((hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex), 16);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const getContentType = (content) => {
|
|
36
|
-
if (!content) return;
|
|
37
|
-
const keys = Object.keys(content);
|
|
38
|
-
return keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export const normalizeMessageContent = (content) => {
|
|
42
|
-
if (!content) return;
|
|
43
|
-
for (let i = 0; i < 5; i++) {
|
|
44
|
-
const inner = content?.ephemeralMessage || content?.viewOnceMessage || content?.documentWithCaptionMessage || content?.viewOnceMessageV2 || content?.viewOnceMessageV2Extension || content?.editedMessage;
|
|
45
|
-
if (!inner) break;
|
|
46
|
-
content = inner.message;
|
|
47
|
-
}
|
|
48
|
-
return content;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const extractMessageContent = (content) => {
|
|
52
|
-
content = normalizeMessageContent(content);
|
|
53
|
-
const extractTemplate = (msg) => msg.imageMessage ? { imageMessage: msg.imageMessage } : msg.documentMessage ? { documentMessage: msg.documentMessage } : msg.videoMessage ? { videoMessage: msg.videoMessage } : msg.locationMessage ? { locationMessage: msg.locationMessage } : { conversation: msg.contentText || msg.hydratedContentText || '' };
|
|
54
|
-
return content?.buttonsMessage ? extractTemplate(content.buttonsMessage) : content?.templateMessage?.hydratedFourRowTemplate ? extractTemplate(content.templateMessage.hydratedFourRowTemplate) : content?.templateMessage?.hydratedTemplate ? extractTemplate(content.templateMessage.hydratedTemplate) : content?.templateMessage?.fourRowTemplate ? extractTemplate(content.templateMessage.fourRowTemplate) : content;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// ===== MEDIA PREPARATION =====
|
|
58
|
-
export const prepareWAMessageMedia = async (message, options) => {
|
|
59
|
-
let mediaType = MEDIA_KEYS.find(key => key in message)
|
|
60
|
-
if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
|
|
61
|
-
|
|
62
|
-
const uploadData = { ...message, media: message[mediaType] }
|
|
63
|
-
delete uploadData[mediaType]
|
|
64
|
-
|
|
65
|
-
const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache ? `${mediaType}:${uploadData.media.url.toString()}` : null
|
|
66
|
-
|
|
67
|
-
if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
|
|
68
|
-
if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
|
69
|
-
|
|
70
|
-
if (cacheableKey) {
|
|
71
|
-
const cached = await options.mediaCache?.get(cacheableKey)
|
|
72
|
-
if (cached) {
|
|
73
|
-
const obj = proto.Message.decode(cached)
|
|
74
|
-
Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
|
|
75
|
-
return obj
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
|
80
|
-
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
|
|
81
|
-
const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor)
|
|
82
|
-
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
|
|
83
|
-
|
|
84
|
-
const encryptionResult = await (options.newsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
|
|
85
|
-
logger: options.logger,
|
|
86
|
-
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
|
|
87
|
-
opts: options.options,
|
|
88
|
-
isPtt: uploadData.ptt,
|
|
89
|
-
forceOpus: mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'),
|
|
90
|
-
convertVideo: mediaType === 'video'
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// ✅ FIX: Extract the correct values based on encryption method
|
|
94
|
-
const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
|
|
95
|
-
|
|
96
|
-
if (mediaType === 'audio' && opusConverted) uploadData.mimetype = 'audio/ogg; codecs=opus'
|
|
97
|
-
|
|
98
|
-
const fileEncSha256B64 = (options.newsletter ? fileSha256 : fileEncSha256 ?? fileSha256).toString('base64')
|
|
99
|
-
|
|
100
|
-
// ✅ FIX: Determine what to upload - use encFilePath for encrypted, encWriteStream for newsletter
|
|
101
|
-
const uploadStream = options.newsletter ? encWriteStream : (encFilePath || encWriteStream)
|
|
102
|
-
|
|
103
|
-
const [{ mediaUrl, directPath, handle }] = await Promise.all([
|
|
104
|
-
(async () => {
|
|
105
|
-
const result = await options.upload(uploadStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
|
|
106
|
-
options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
|
|
107
|
-
return result
|
|
108
|
-
})(),
|
|
109
|
-
(async () => {
|
|
110
|
-
try {
|
|
111
|
-
if (requiresThumbnailComputation) {
|
|
112
|
-
const { thumbnail, originalImageDimensions } = await generateThumbnail(bodyPath, mediaType, options)
|
|
113
|
-
uploadData.jpegThumbnail = thumbnail
|
|
114
|
-
if (!uploadData.width && originalImageDimensions) {
|
|
115
|
-
uploadData.width = originalImageDimensions.width
|
|
116
|
-
uploadData.height = originalImageDimensions.height
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
|
|
120
|
-
if (requiresWaveformProcessing) {
|
|
121
|
-
try {
|
|
122
|
-
uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
|
|
123
|
-
} catch (err) {
|
|
124
|
-
options.logger?.warn('Failed to generate waveform, using fallback')
|
|
125
|
-
uploadData.waveform = new Uint8Array([0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99,0,99,0,99,0,99,0,99,88,99,0,99,0,55,0,99])
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (options.backgroundColor && mediaType === 'audio') uploadData.backgroundArgb = assertColor(options.backgroundColor)
|
|
129
|
-
} catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info') }
|
|
130
|
-
})()
|
|
131
|
-
]).finally(async () => {
|
|
132
|
-
if (encWriteStream && !Buffer.isBuffer(encWriteStream)) encWriteStream.destroy?.()
|
|
133
|
-
// ✅ FIX: Clean up encrypted file path
|
|
134
|
-
if (encFilePath && typeof encFilePath === 'string') {
|
|
135
|
-
try {
|
|
136
|
-
await fs.unlink(encFilePath)
|
|
137
|
-
} catch {}
|
|
138
|
-
}
|
|
139
|
-
if (didSaveToTmpPath && bodyPath) {
|
|
140
|
-
try {
|
|
141
|
-
await fs.access(bodyPath)
|
|
142
|
-
await fs.unlink(bodyPath)
|
|
143
|
-
} catch { }
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
const obj = WAProto.Message.fromObject({
|
|
148
|
-
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
|
|
149
|
-
url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
|
|
150
|
-
mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined
|
|
151
|
-
})
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
|
|
155
|
-
if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
|
|
156
|
-
return obj
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
|
|
160
|
-
ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
export const generateForwardMessageContent = (message, forceForward) => {
|
|
164
|
-
const content = proto.Message.decode(proto.Message.encode(normalizeMessageContent(message.message)).finish());
|
|
165
|
-
let key = Object.keys(content)[0];
|
|
166
|
-
let score = (content?.[key]?.contextInfo?.forwardingScore || 0) + (message.key.fromMe && !forceForward ? 0 : 1);
|
|
167
|
-
|
|
168
|
-
if (key === 'conversation') {
|
|
169
|
-
content.extendedTextMessage = { text: content[key] };
|
|
170
|
-
delete content.conversation;
|
|
171
|
-
key = 'extendedTextMessage';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
|
|
175
|
-
return content;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// ===== MESSAGE HANDLERS =====
|
|
179
|
-
const handleTextMessage = async (message, options) => {
|
|
180
|
-
const extContent = { text: message.text };
|
|
181
|
-
let urlInfo = message.linkPreview || await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
|
|
182
|
-
|
|
183
|
-
if (urlInfo) {
|
|
184
|
-
Object.assign(extContent, {
|
|
185
|
-
matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
|
|
186
|
-
description: urlInfo.description, title: urlInfo.title, previewType: 0
|
|
187
|
-
});
|
|
188
|
-
if (urlInfo.highQualityThumbnail) {
|
|
189
|
-
const img = urlInfo.highQualityThumbnail;
|
|
190
|
-
Object.assign(extContent, {
|
|
191
|
-
thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
|
|
192
|
-
thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
|
|
198
|
-
if (options.font) extContent.font = options.font;
|
|
199
|
-
return { extendedTextMessage: extContent };
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const handleSpecialMessages = async (message, options) => {
|
|
203
|
-
if ('contacts' in message) {
|
|
204
|
-
const { contacts } = message.contacts;
|
|
205
|
-
if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 });
|
|
206
|
-
return contacts.length === 1 ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) } : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
|
|
207
|
-
}
|
|
208
|
-
if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
|
|
209
|
-
if ('react' in message) {
|
|
210
|
-
if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now();
|
|
211
|
-
return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) };
|
|
212
|
-
}
|
|
213
|
-
if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } };
|
|
214
|
-
if ('forward' in message) return generateForwardMessageContent(message.forward, message.force);
|
|
215
|
-
if ('disappearingMessagesInChat' in message) {
|
|
216
|
-
const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
|
|
217
|
-
return prepareDisappearingMessageSettingContent(exp);
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const handleGroupInvite = async (message, options) => {
|
|
223
|
-
const m = {
|
|
224
|
-
groupInviteMessage: {
|
|
225
|
-
inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
|
|
226
|
-
caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
if (options.getProfilePicUrl) {
|
|
231
|
-
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
|
|
232
|
-
if (pfpUrl) {
|
|
233
|
-
const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
|
|
234
|
-
if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer());
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return m;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const handleEventMessage = (message, options) => {
|
|
241
|
-
const startTime = Math.floor(message.event.startDate.getTime() / 1000);
|
|
242
|
-
const m = {
|
|
243
|
-
eventMessage: {
|
|
244
|
-
name: message.event.name, description: message.event.description, startTime,
|
|
245
|
-
endTime: message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined,
|
|
246
|
-
isCanceled: message.event.isCancelled ?? false, extraGuestsAllowed: message.event.extraGuestsAllowed,
|
|
247
|
-
isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
|
|
248
|
-
},
|
|
249
|
-
messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
if (message.event.call && options.getCallLink) {
|
|
253
|
-
options.getCallLink(message.event.call, { startTime }).then(token => {
|
|
254
|
-
m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
return m;
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const handlePollMessage = (message) => {
|
|
261
|
-
message.poll.selectableCount ||= 0;
|
|
262
|
-
message.poll.toAnnouncementGroup ||= false;
|
|
263
|
-
|
|
264
|
-
if (!Array.isArray(message.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 });
|
|
265
|
-
if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length)
|
|
266
|
-
throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
|
|
267
|
-
|
|
268
|
-
const pollMsg = { name: message.poll.name, selectableOptionsCount: message.poll.selectableCount, options: message.poll.values.map(optionName => ({ optionName })) };
|
|
269
|
-
const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } };
|
|
270
|
-
if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg;
|
|
271
|
-
else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg;
|
|
272
|
-
else m.pollCreationMessage = pollMsg;
|
|
273
|
-
return m;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
const handleProductMessage = async (message, options) => {
|
|
277
|
-
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
|
|
278
|
-
return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) };
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
const handleRequestPayment = async (message, options) => {
|
|
282
|
-
const sticker = message.requestPayment.sticker ? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options) : null;
|
|
283
|
-
let notes = message.requestPayment.sticker
|
|
284
|
-
? { stickerMessage: { ...sticker.stickerMessage, contextInfo: message.requestPayment.contextInfo } }
|
|
285
|
-
: message.requestPayment.note ? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } } : null;
|
|
286
|
-
|
|
287
|
-
if (!notes) throw new Boom('Invalid request payment', { statusCode: 400 });
|
|
288
|
-
|
|
289
|
-
const m = {
|
|
290
|
-
requestPaymentMessage: WAProto.Message.RequestPaymentMessage.fromObject({
|
|
291
|
-
expiryTimestamp: message.requestPayment.expiryTimestamp || message.requestPayment.expiry,
|
|
292
|
-
amount1000: message.requestPayment.amount1000 || message.requestPayment.amount,
|
|
293
|
-
currencyCodeIso4217: message.requestPayment.currencyCodeIso4217 || message.requestPayment.currency,
|
|
294
|
-
requestFrom: message.requestPayment.requestFrom || message.requestPayment.from,
|
|
295
|
-
noteMessage: notes, background: message.requestPayment.background
|
|
296
|
-
})
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
if (message.requestPayment.currencyCodeIso4217 === 'BRL' && message.requestPayment.pixKey) {
|
|
300
|
-
if (!m.requestPaymentMessage.noteMessage.extendedTextMessage)
|
|
301
|
-
m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } };
|
|
302
|
-
m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${message.requestPayment.pixKey}`;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return m;
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
// ===== MAIN GENERATOR =====
|
|
309
|
-
export const generateWAMessageContent = async (message, options = {}) => {
|
|
310
|
-
const messageKeys = Object.keys(message);
|
|
311
|
-
|
|
312
|
-
// ===== SMART DETECTION =====
|
|
313
|
-
const isRawProtoMessage = messageKeys.some(key =>
|
|
314
|
-
key.endsWith('Message') &&
|
|
315
|
-
typeof message[key] === 'object' &&
|
|
316
|
-
!HIGH_LEVEL_KEYS.includes(key)
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message);
|
|
320
|
-
|
|
321
|
-
// Pass through raw protocol messages directly
|
|
322
|
-
if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) {
|
|
323
|
-
return WAProto.Message.create(message);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// If no high-level keys AND has proto message keys, pass through
|
|
327
|
-
if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) {
|
|
328
|
-
return WAProto.Message.create(message);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
let m = {};
|
|
332
|
-
|
|
333
|
-
// ===== HANDLE TEXT =====
|
|
334
|
-
if ('text' in message && !('buttons' in message) && !('templateButtons' in message) && !('sections' in message) && !('interactiveButtons' in message) && !('shop' in message)) {
|
|
335
|
-
m = await handleTextMessage(message, options);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ===== HANDLE SPECIAL MESSAGES =====
|
|
339
|
-
else {
|
|
340
|
-
const special = await handleSpecialMessages(message, options);
|
|
341
|
-
if (special) m = special;
|
|
342
|
-
else if ('groupInvite' in message) m = await handleGroupInvite(message, options);
|
|
343
|
-
else if ('stickerPack' in message) return await prepareStickerPackMessage(message.stickerPack, options);
|
|
344
|
-
else if ('pin' in message) {
|
|
345
|
-
const messageKey = typeof message.pin === 'boolean' ? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation'); })()) : (message.pin && typeof message.pin === 'object') ? (message.pin.key || message.pin.stanzaId || (message.pin.id ? { remoteJid: options.jid, fromMe: message.pin.fromMe || false, id: message.pin.id, participant: message.pin.participant || message.pin.sender } : null)) : message.pin;
|
|
346
|
-
const shouldPin = typeof message.pin === 'boolean' ? message.pin : (message.pin && typeof message.pin === 'object' ? message.pin.unpin !== true : true);
|
|
347
|
-
const pinTime = message.pin && typeof message.pin === 'object' ? message.pin.time : message.time;
|
|
348
|
-
if (!messageKey || !messageKey.id) throw new Boom('Invalid message key for pin operation');
|
|
349
|
-
m = { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (pinTime || 86400) : 0 } };
|
|
350
|
-
}
|
|
351
|
-
else if ('keep' in message) m = { keepInChatMessage: { key: message.keep, keepType: message.type, timestampMs: Date.now() } };
|
|
352
|
-
else if ('call' in message) m = { scheduledCallCreationMessage: { scheduledTimestampMs: message.call.time || Date.now(), callType: message.call.type || 1, title: message.call.title } };
|
|
353
|
-
else if ('paymentInvite' in message) m = { paymentInviteMessage: { serviceType: message.paymentInvite.type, expiryTimestamp: message.paymentInvite.expiry } };
|
|
354
|
-
else if ('buttonReply' in message) m = message.type === 'template' ? { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } } : { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: 0 } };
|
|
355
|
-
else if ('ptv' in message && message.ptv) {
|
|
356
|
-
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
|
|
357
|
-
m = { ptvMessage: videoMessage };
|
|
358
|
-
}
|
|
359
|
-
else if ('product' in message) m = await handleProductMessage(message, options);
|
|
360
|
-
else if ('order' in message) m = { orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: message.order.id, thumbnail: message.order.thumbnail, itemCount: message.order.itemCount, status: message.order.status, surface: message.order.surface, orderTitle: message.order.title, message: message.order.text, sellerJid: message.order.seller, token: message.order.token, totalAmount1000: message.order.amount, totalCurrencyCode: message.order.currency }) };
|
|
361
|
-
else if ('listReply' in message) m = { listResponseMessage: { ...message.listReply } };
|
|
362
|
-
else if ('event' in message) m = handleEventMessage(message, options);
|
|
363
|
-
else if ('poll' in message) m = handlePollMessage(message);
|
|
364
|
-
else if ('inviteAdmin' in message) m = { newsletterAdminInviteMessage: { inviteExpiration: message.inviteAdmin.inviteExpiration, caption: message.inviteAdmin.text, newsletterJid: message.inviteAdmin.jid, newsletterName: message.inviteAdmin.subject, jpegThumbnail: message.inviteAdmin.thumbnail } };
|
|
365
|
-
else if ('requestPayment' in message) m = await handleRequestPayment(message, options);
|
|
366
|
-
else if ('extendedTextMessage' in message) m = { extendedTextMessage: WAProto.Message.ExtendedTextMessage.create(message.extendedTextMessage) };
|
|
367
|
-
else if ('interactiveMessage' in message) m = { interactiveMessage: WAProto.Message.InteractiveMessage.create(message.interactiveMessage) };
|
|
368
|
-
else if ('sharePhoneNumber' in message) m = { protocolMessage: { type: 4 } };
|
|
369
|
-
else if ('requestPhoneNumber' in message) m = { requestPhoneNumberMessage: {} };
|
|
370
|
-
else if ('limitSharing' in message) m = { protocolMessage: { type: 3, limitSharing: { sharingLimited: message.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } };
|
|
371
|
-
else if ('album' in message) {
|
|
372
|
-
const imageMessages = message.album.filter(item => 'image' in item);
|
|
373
|
-
const videoMessages = message.album.filter(item => 'video' in item);
|
|
374
|
-
m = { albumMessage: { expectedImageCount: imageMessages.length, expectedVideoCount: videoMessages.length } };
|
|
375
|
-
}
|
|
376
|
-
else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ===== SMART BUTTON HANDLING =====
|
|
380
|
-
if ('buttons' in message && Array.isArray(message.buttons) && message.buttons.length > 0) {
|
|
381
|
-
const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson);
|
|
382
|
-
|
|
383
|
-
if (hasNativeFlow) {
|
|
384
|
-
// Convert to interactiveMessage
|
|
385
|
-
const interactive = {
|
|
386
|
-
body: { text: message.text || message.caption || message.contentText || '' },
|
|
387
|
-
footer: { text: message.footer || message.footerText || '' },
|
|
388
|
-
nativeFlowMessage: {
|
|
389
|
-
buttons: message.buttons.map(btn => {
|
|
390
|
-
if (btn.name && btn.buttonParamsJson) return btn;
|
|
391
|
-
if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson };
|
|
392
|
-
return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) };
|
|
393
|
-
})
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
|
|
398
|
-
if (Object.keys(m).length > 0) {
|
|
399
|
-
interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true };
|
|
400
|
-
Object.assign(interactive.header, m);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
m = { interactiveMessage: interactive };
|
|
404
|
-
} else {
|
|
405
|
-
// Old-style buttons
|
|
406
|
-
const buttonsMessage = { buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) };
|
|
407
|
-
if ('text' in message) { buttonsMessage.contentText = message.text; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY; }
|
|
408
|
-
else { if ('caption' in message) buttonsMessage.contentText = message.caption; const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase(); buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY; Object.assign(buttonsMessage, m); }
|
|
409
|
-
if (message.title) { buttonsMessage.text = message.title; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT; }
|
|
410
|
-
if (message.footer) buttonsMessage.footerText = message.footer;
|
|
411
|
-
m = { buttonsMessage };
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ===== TEMPLATE BUTTONS =====
|
|
416
|
-
else if ('templateButtons' in message && !!message.templateButtons) {
|
|
417
|
-
const msg = { hydratedButtons: message.templateButtons };
|
|
418
|
-
if ('text' in message) msg.hydratedContentText = message.text;
|
|
419
|
-
else { if ('caption' in message) msg.hydratedContentText = message.caption; Object.assign(msg, m); }
|
|
420
|
-
if ('footer' in message && !!message.footer) msg.hydratedFooterText = message.footer;
|
|
421
|
-
m = { templateMessage: { fourRowTemplate: msg, hydratedTemplate: msg } };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// ===== LIST MESSAGE =====
|
|
425
|
-
else if ('sections' in message && !!message.sections) {
|
|
426
|
-
m = { listMessage: { sections: message.sections, buttonText: message.buttonText, title: message.title, footerText: message.footer, description: message.text, listType: proto.Message.ListMessage.ListType.SINGLE_SELECT } };
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ===== INTERACTIVE BUTTONS =====
|
|
430
|
-
else if ('interactiveButtons' in message && !!message.interactiveButtons) {
|
|
431
|
-
const interactiveMessage = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: message.interactiveButtons }) };
|
|
432
|
-
if ('text' in message) interactiveMessage.body = { text: message.text };
|
|
433
|
-
else if ('caption' in message) { interactiveMessage.body = { text: message.caption }; interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
|
|
434
|
-
if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
|
|
435
|
-
if ('title' in message && !!message.title) { interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
|
|
436
|
-
m = { interactiveMessage };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// ===== SHOP MESSAGE (YOUR EXAMPLE) =====
|
|
440
|
-
else if ('shop' in message && !!message.shop) {
|
|
441
|
-
const interactiveMessage = {
|
|
442
|
-
shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({
|
|
443
|
-
surface: message.shop.surface || 1,
|
|
444
|
-
id: message.shop.id || message.id
|
|
445
|
-
})
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
// Handle body text
|
|
449
|
-
if ('text' in message) interactiveMessage.body = { text: message.text };
|
|
450
|
-
else if ('caption' in message) interactiveMessage.body = { text: message.caption };
|
|
451
|
-
|
|
452
|
-
// Handle header with media
|
|
453
|
-
if (message.title || message.subtitle || Object.keys(m).length > 0) {
|
|
454
|
-
interactiveMessage.header = {
|
|
455
|
-
title: message.title || '',
|
|
456
|
-
subtitle: message.subtitle || '',
|
|
457
|
-
hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0)
|
|
458
|
-
};
|
|
459
|
-
if (Object.keys(m).length > 0) Object.assign(interactiveMessage.header, m);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
|
|
463
|
-
|
|
464
|
-
m = { interactiveMessage };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// ===== AUTO-APPLY CONTEXT & WRAPPERS =====
|
|
468
|
-
const finalKey = Object.keys(m)[0];
|
|
469
|
-
|
|
470
|
-
// Auto-merge contextInfo and mentions
|
|
471
|
-
if ((message.contextInfo || message.mentions) && finalKey && m[finalKey]) {
|
|
472
|
-
m[finalKey].contextInfo = {
|
|
473
|
-
...(m[finalKey].contextInfo || {}),
|
|
474
|
-
...(message.contextInfo || {}),
|
|
475
|
-
mentionedJid: message.mentions || message.contextInfo?.mentionedJid || []
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// ViewOnce wrapper
|
|
480
|
-
if (message.viewOnce === true) m = { viewOnceMessage: { message: m } };
|
|
481
|
-
|
|
482
|
-
// Edit wrapper
|
|
483
|
-
if (message.edit) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } };
|
|
484
|
-
|
|
485
|
-
return WAProto.Message.create(m);
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
export const generateWAMessageFromContent = (jid, message, options) => {
|
|
490
|
-
if (!options.timestamp) options.timestamp = new Date();
|
|
491
|
-
const innerMessage = normalizeMessageContent(message);
|
|
492
|
-
const key = getContentType(innerMessage);
|
|
493
|
-
const timestamp = unixTimestampSeconds(options.timestamp);
|
|
494
|
-
const { quoted, userJid } = options;
|
|
495
|
-
|
|
496
|
-
if (quoted && !isJidNewsletter(jid)) {
|
|
497
|
-
const participant = quoted.key.fromMe ? userJid : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
|
|
498
|
-
const quotedMsg = proto.Message.create({ [getContentType(normalizeMessageContent(quoted.message))]: normalizeMessageContent(quoted.message)[getContentType(normalizeMessageContent(quoted.message))] });
|
|
499
|
-
const contextInfo = (innerMessage[key]?.contextInfo) || {};
|
|
500
|
-
contextInfo.participant = jidNormalizedUser(participant);
|
|
501
|
-
contextInfo.stanzaId = quoted.key.id;
|
|
502
|
-
contextInfo.quotedMessage = quotedMsg;
|
|
503
|
-
if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid;
|
|
504
|
-
innerMessage[key].contextInfo = contextInfo;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
|
|
508
|
-
innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return WAProto.WebMessageInfo.fromObject({
|
|
512
|
-
key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
|
|
513
|
-
message: WAProto.Message.create(message),
|
|
514
|
-
messageTimestamp: timestamp,
|
|
515
|
-
messageStubParameters: [],
|
|
516
|
-
participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
|
|
517
|
-
status: WAMessageStatus.PENDING
|
|
518
|
-
});
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
export const generateWAMessage = async (jid, content, options = {}) => {
|
|
522
|
-
options.logger = options?.logger?.child({ msgId: options.messageId });
|
|
523
|
-
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
// ===== UTILITIES =====
|
|
527
|
-
export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown';
|
|
528
|
-
|
|
529
|
-
export const updateMessageWithReceipt = (msg, receipt) => {
|
|
530
|
-
msg.userReceipt ||= [];
|
|
531
|
-
const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
|
|
532
|
-
if (recp) Object.assign(recp, receipt);
|
|
533
|
-
else msg.userReceipt.push(receipt);
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
export const updateMessageWithReaction = (msg, reaction) => {
|
|
537
|
-
const authorID = getKeyAuthor(reaction.key);
|
|
538
|
-
msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
|
|
539
|
-
reaction.text ||= '';
|
|
540
|
-
msg.reactions.push(reaction);
|
|
541
|
-
};
|
|
542
|
-
|
|
543
|
-
export const updateMessageWithPollUpdate = (msg, update) => {
|
|
544
|
-
const authorID = getKeyAuthor(update.pollUpdateMessageKey);
|
|
545
|
-
msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
|
|
546
|
-
if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update);
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
|
|
550
|
-
const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || [];
|
|
551
|
-
const voteHashMap = opts.reduce((acc, opt) => {
|
|
552
|
-
const hash = sha256(Buffer.from(opt.optionName || '')).toString();
|
|
553
|
-
acc[hash] = { name: opt.optionName || '', voters: [] };
|
|
554
|
-
return acc;
|
|
555
|
-
}, {});
|
|
556
|
-
|
|
557
|
-
for (const update of pollUpdates || []) {
|
|
558
|
-
const { vote } = update;
|
|
559
|
-
if (!vote) continue;
|
|
560
|
-
for (const option of vote.selectedOptions || []) {
|
|
561
|
-
const hash = option.toString();
|
|
562
|
-
voteHashMap[hash] ||= { name: 'Unknown', voters: [] };
|
|
563
|
-
voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
return Object.values(voteHashMap);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
export const aggregateMessageKeysNotFromMe = (keys) => {
|
|
570
|
-
const keyMap = {};
|
|
571
|
-
for (const { remoteJid, id, participant, fromMe } of keys) {
|
|
572
|
-
if (!fromMe) {
|
|
573
|
-
const uqKey = `${remoteJid}:${participant || ''}`;
|
|
574
|
-
keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] };
|
|
575
|
-
keyMap[uqKey].messageIds.push(id);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return Object.values(keyMap);
|
|
579
|
-
};
|
|
580
|
-
|
|
581
|
-
const REUPLOAD_STATUS = [410, 404];
|
|
582
|
-
|
|
583
|
-
export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
584
|
-
const downloadMsg = async () => {
|
|
585
|
-
let normalizedMessage = message;
|
|
586
|
-
if (!message.message && message.key && message.participant) normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
|
|
587
|
-
if (!normalizedMessage.message && typeof message === 'object') {
|
|
588
|
-
const possibleMessage = message.message || message.quoted?.message || message;
|
|
589
|
-
normalizedMessage = { key: message.key || {}, message: possibleMessage, messageTimestamp: message.messageTimestamp };
|
|
590
|
-
}
|
|
591
|
-
const mContent = extractMessageContent(normalizedMessage.message);
|
|
592
|
-
if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message });
|
|
593
|
-
const contentType = getContentType(mContent);
|
|
594
|
-
let mediaType = contentType?.replace('Message', '');
|
|
595
|
-
const media = mContent[contentType];
|
|
596
|
-
if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media)))
|
|
597
|
-
throw new Boom(`"${contentType}" is not a media message`);
|
|
598
|
-
const download = 'thumbnailDirectPath' in media && !('url' in media) ? { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey } : media;
|
|
599
|
-
const stream = await downloadContentFromMessage(download, mediaType, options);
|
|
600
|
-
if (type === 'buffer') {
|
|
601
|
-
const chunks = [];
|
|
602
|
-
for await (const chunk of stream) chunks.push(chunk);
|
|
603
|
-
return Buffer.concat(chunks);
|
|
604
|
-
}
|
|
605
|
-
return stream;
|
|
606
|
-
};
|
|
607
|
-
return downloadMsg().catch(async (error) => {
|
|
608
|
-
if (ctx && typeof error?.status === 'number' && REUPLOAD_STATUS.includes(error.status)) {
|
|
609
|
-
message = await ctx.reuploadRequest(message);
|
|
610
|
-
return downloadMsg();
|
|
611
|
-
}
|
|
612
|
-
throw error;
|
|
613
|
-
});
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
export async function prepareStickerPackMessage(stickerPack, options) {
|
|
617
|
-
const { stickers, name, publisher, packId, description } = stickerPack;
|
|
618
|
-
if (!stickers?.length) throw new Boom('Sticker pack requires at least one sticker', { statusCode: 400 });
|
|
619
|
-
|
|
620
|
-
const lib = await getImageProcessingLibrary();
|
|
621
|
-
const packId_ = packId || generateMessageIDV2();
|
|
622
|
-
const validStickers = [];
|
|
623
|
-
|
|
624
|
-
for (const s of stickers) {
|
|
625
|
-
try {
|
|
626
|
-
const { stream } = await getStream(s.data);
|
|
627
|
-
let buffer = await toBuffer(stream);
|
|
628
|
-
const isWebP = buffer.length >= 12 && buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46;
|
|
629
|
-
if (!isWebP) {
|
|
630
|
-
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp().toBuffer();
|
|
631
|
-
else if ('jimp' in lib) buffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'));
|
|
632
|
-
}
|
|
633
|
-
if (buffer.length > 1024 * 1024) {
|
|
634
|
-
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer();
|
|
635
|
-
if (buffer.length > 1024 * 1024) continue;
|
|
636
|
-
}
|
|
637
|
-
validStickers.push({
|
|
638
|
-
fileName: `${sha256(buffer).toString('base64').replace(/\//g, '-')}.webp`,
|
|
639
|
-
buffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false,
|
|
640
|
-
emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel
|
|
641
|
-
});
|
|
642
|
-
} catch (e) { options.logger?.warn(`Sticker failed: ${e.message}`); }
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (!validStickers.length) throw new Boom('No valid stickers', { statusCode: 400 });
|
|
646
|
-
|
|
647
|
-
const { stream: covStream } = await getStream(stickerPack.cover);
|
|
648
|
-
let coverBuffer = await toBuffer(covStream);
|
|
649
|
-
const isWebPCover = coverBuffer.length >= 12 && coverBuffer[0] === 0x52 && coverBuffer[1] === 0x49 && coverBuffer[2] === 0x46 && coverBuffer[3] === 0x46;
|
|
650
|
-
if (!isWebPCover) {
|
|
651
|
-
if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
|
|
652
|
-
else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const processBatch = async (batch, batchIdx) => {
|
|
656
|
-
const batchData = {};
|
|
657
|
-
batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
|
|
658
|
-
const trayFile = `${packId_}_batch${batchIdx}.webp`;
|
|
659
|
-
batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
|
|
660
|
-
|
|
661
|
-
const zipBuf = await new Promise((resolve, reject) => { zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))); });
|
|
662
|
-
const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
|
|
663
|
-
const uploadRes = await options.upload(upload.encFilePath, {
|
|
664
|
-
fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
665
|
-
});
|
|
666
|
-
await fs.unlink(upload.encFilePath);
|
|
667
|
-
|
|
668
|
-
let thumbBuf;
|
|
669
|
-
if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
|
|
670
|
-
else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
|
|
671
|
-
|
|
672
|
-
let thumbUploadRes;
|
|
673
|
-
if (thumbBuf?.length) {
|
|
674
|
-
const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
|
|
675
|
-
thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
|
|
676
|
-
fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
677
|
-
});
|
|
678
|
-
await fs.unlink(thumbUpload.encFilePath);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
return {
|
|
682
|
-
name: `${name} (${batchIdx + 1})`, publisher, packDescription: description, stickerPackId: `${packId_}_${batchIdx}`,
|
|
683
|
-
stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
|
|
684
|
-
stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
|
|
685
|
-
fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
|
|
686
|
-
directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
|
|
687
|
-
...(thumbUploadRes && { thumbnailDirectPath: thumbUploadRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined })
|
|
688
|
-
};
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
if (validStickers.length > 60) {
|
|
692
|
-
const batches = [];
|
|
693
|
-
for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
|
|
694
|
-
const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
|
|
695
|
-
return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
export const assertMediaContent = (content) => {
|
|
702
|
-
content = extractMessageContent(content);
|
|
703
|
-
const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage;
|
|
704
|
-
if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content });
|
|
705
|
-
return mediaContent;
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import { zip } from 'fflate';
|
|
5
|
+
import { proto } from '../../WAProto/index.js';
|
|
6
|
+
import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
|
|
7
|
+
import { WAMessageStatus, WAProto } from '../Types/index.js';
|
|
8
|
+
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
|
+
import { sha256 } from './crypto.js';
|
|
10
|
+
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
|
|
11
|
+
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
|
|
12
|
+
|
|
13
|
+
const MIMETYPE_MAP = { image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf', audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg' };
|
|
14
|
+
const MessageTypeProto = { image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage, audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage, document: WAProto.Message.DocumentMessage };
|
|
15
|
+
|
|
16
|
+
// High-level content keys that need processing (not raw WAProto)
|
|
17
|
+
const HIGH_LEVEL_KEYS = ['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'];
|
|
18
|
+
|
|
19
|
+
// ===== UTILITIES =====
|
|
20
|
+
export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
|
|
21
|
+
|
|
22
|
+
export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
|
|
23
|
+
const url = extractUrlFromText(text);
|
|
24
|
+
if (!getUrlInfo || !url) return;
|
|
25
|
+
try { return await getUrlInfo(url); }
|
|
26
|
+
catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed'); }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const assertColor = (color) => {
|
|
30
|
+
if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1;
|
|
31
|
+
let hex = color.trim().replace('#', '');
|
|
32
|
+
return parseInt((hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex), 16);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getContentType = (content) => {
|
|
36
|
+
if (!content) return;
|
|
37
|
+
const keys = Object.keys(content);
|
|
38
|
+
return keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const normalizeMessageContent = (content) => {
|
|
42
|
+
if (!content) return;
|
|
43
|
+
for (let i = 0; i < 5; i++) {
|
|
44
|
+
const inner = content?.ephemeralMessage || content?.viewOnceMessage || content?.documentWithCaptionMessage || content?.viewOnceMessageV2 || content?.viewOnceMessageV2Extension || content?.editedMessage;
|
|
45
|
+
if (!inner) break;
|
|
46
|
+
content = inner.message;
|
|
47
|
+
}
|
|
48
|
+
return content;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const extractMessageContent = (content) => {
|
|
52
|
+
content = normalizeMessageContent(content);
|
|
53
|
+
const extractTemplate = (msg) => msg.imageMessage ? { imageMessage: msg.imageMessage } : msg.documentMessage ? { documentMessage: msg.documentMessage } : msg.videoMessage ? { videoMessage: msg.videoMessage } : msg.locationMessage ? { locationMessage: msg.locationMessage } : { conversation: msg.contentText || msg.hydratedContentText || '' };
|
|
54
|
+
return content?.buttonsMessage ? extractTemplate(content.buttonsMessage) : content?.templateMessage?.hydratedFourRowTemplate ? extractTemplate(content.templateMessage.hydratedFourRowTemplate) : content?.templateMessage?.hydratedTemplate ? extractTemplate(content.templateMessage.hydratedTemplate) : content?.templateMessage?.fourRowTemplate ? extractTemplate(content.templateMessage.fourRowTemplate) : content;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ===== MEDIA PREPARATION =====
|
|
58
|
+
export const prepareWAMessageMedia = async (message, options) => {
|
|
59
|
+
let mediaType = MEDIA_KEYS.find(key => key in message)
|
|
60
|
+
if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
|
|
61
|
+
|
|
62
|
+
const uploadData = { ...message, media: message[mediaType] }
|
|
63
|
+
delete uploadData[mediaType]
|
|
64
|
+
|
|
65
|
+
const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache ? `${mediaType}:${uploadData.media.url.toString()}` : null
|
|
66
|
+
|
|
67
|
+
if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
|
|
68
|
+
if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
|
69
|
+
|
|
70
|
+
if (cacheableKey) {
|
|
71
|
+
const cached = await options.mediaCache?.get(cacheableKey)
|
|
72
|
+
if (cached) {
|
|
73
|
+
const obj = proto.Message.decode(cached)
|
|
74
|
+
Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
|
|
75
|
+
return obj
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
|
80
|
+
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
|
|
81
|
+
const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor)
|
|
82
|
+
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
|
|
83
|
+
|
|
84
|
+
const encryptionResult = await (options.newsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
|
|
85
|
+
logger: options.logger,
|
|
86
|
+
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
|
|
87
|
+
opts: options.options,
|
|
88
|
+
isPtt: uploadData.ptt,
|
|
89
|
+
forceOpus: mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'),
|
|
90
|
+
convertVideo: mediaType === 'video'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ✅ FIX: Extract the correct values based on encryption method
|
|
94
|
+
const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
|
|
95
|
+
|
|
96
|
+
if (mediaType === 'audio' && opusConverted) uploadData.mimetype = 'audio/ogg; codecs=opus'
|
|
97
|
+
|
|
98
|
+
const fileEncSha256B64 = (options.newsletter ? fileSha256 : fileEncSha256 ?? fileSha256).toString('base64')
|
|
99
|
+
|
|
100
|
+
// ✅ FIX: Determine what to upload - use encFilePath for encrypted, encWriteStream for newsletter
|
|
101
|
+
const uploadStream = options.newsletter ? encWriteStream : (encFilePath || encWriteStream)
|
|
102
|
+
|
|
103
|
+
const [{ mediaUrl, directPath, handle }] = await Promise.all([
|
|
104
|
+
(async () => {
|
|
105
|
+
const result = await options.upload(uploadStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
|
|
106
|
+
options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
|
|
107
|
+
return result
|
|
108
|
+
})(),
|
|
109
|
+
(async () => {
|
|
110
|
+
try {
|
|
111
|
+
if (requiresThumbnailComputation) {
|
|
112
|
+
const { thumbnail, originalImageDimensions } = await generateThumbnail(bodyPath, mediaType, options)
|
|
113
|
+
uploadData.jpegThumbnail = thumbnail
|
|
114
|
+
if (!uploadData.width && originalImageDimensions) {
|
|
115
|
+
uploadData.width = originalImageDimensions.width
|
|
116
|
+
uploadData.height = originalImageDimensions.height
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
|
|
120
|
+
if (requiresWaveformProcessing) {
|
|
121
|
+
try {
|
|
122
|
+
uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
|
|
123
|
+
} catch (err) {
|
|
124
|
+
options.logger?.warn('Failed to generate waveform, using fallback')
|
|
125
|
+
uploadData.waveform = new Uint8Array([0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99])
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (options.backgroundColor && mediaType === 'audio') uploadData.backgroundArgb = assertColor(options.backgroundColor)
|
|
129
|
+
} catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info') }
|
|
130
|
+
})()
|
|
131
|
+
]).finally(async () => {
|
|
132
|
+
if (encWriteStream && !Buffer.isBuffer(encWriteStream)) encWriteStream.destroy?.()
|
|
133
|
+
// ✅ FIX: Clean up encrypted file path
|
|
134
|
+
if (encFilePath && typeof encFilePath === 'string') {
|
|
135
|
+
try {
|
|
136
|
+
await fs.unlink(encFilePath)
|
|
137
|
+
} catch { }
|
|
138
|
+
}
|
|
139
|
+
if (didSaveToTmpPath && bodyPath) {
|
|
140
|
+
try {
|
|
141
|
+
await fs.access(bodyPath)
|
|
142
|
+
await fs.unlink(bodyPath)
|
|
143
|
+
} catch { }
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const obj = WAProto.Message.fromObject({
|
|
148
|
+
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
|
|
149
|
+
url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
|
|
150
|
+
mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
|
|
155
|
+
if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
|
|
156
|
+
return obj
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
|
|
160
|
+
ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export const generateForwardMessageContent = (message, forceForward) => {
|
|
164
|
+
const content = proto.Message.decode(proto.Message.encode(normalizeMessageContent(message.message)).finish());
|
|
165
|
+
let key = Object.keys(content)[0];
|
|
166
|
+
let score = (content?.[key]?.contextInfo?.forwardingScore || 0) + (message.key.fromMe && !forceForward ? 0 : 1);
|
|
167
|
+
|
|
168
|
+
if (key === 'conversation') {
|
|
169
|
+
content.extendedTextMessage = { text: content[key] };
|
|
170
|
+
delete content.conversation;
|
|
171
|
+
key = 'extendedTextMessage';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
|
|
175
|
+
return content;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ===== MESSAGE HANDLERS =====
|
|
179
|
+
const handleTextMessage = async (message, options) => {
|
|
180
|
+
const extContent = { text: message.text };
|
|
181
|
+
let urlInfo = message.linkPreview || await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
|
|
182
|
+
|
|
183
|
+
if (urlInfo) {
|
|
184
|
+
Object.assign(extContent, {
|
|
185
|
+
matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
|
|
186
|
+
description: urlInfo.description, title: urlInfo.title, previewType: 0
|
|
187
|
+
});
|
|
188
|
+
if (urlInfo.highQualityThumbnail) {
|
|
189
|
+
const img = urlInfo.highQualityThumbnail;
|
|
190
|
+
Object.assign(extContent, {
|
|
191
|
+
thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
|
|
192
|
+
thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
|
|
198
|
+
if (options.font) extContent.font = options.font;
|
|
199
|
+
return { extendedTextMessage: extContent };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleSpecialMessages = async (message, options) => {
|
|
203
|
+
if ('contacts' in message) {
|
|
204
|
+
const { contacts } = message.contacts;
|
|
205
|
+
if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 });
|
|
206
|
+
return contacts.length === 1 ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) } : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
|
|
207
|
+
}
|
|
208
|
+
if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
|
|
209
|
+
if ('react' in message) {
|
|
210
|
+
if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now();
|
|
211
|
+
return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) };
|
|
212
|
+
}
|
|
213
|
+
if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } };
|
|
214
|
+
if ('forward' in message) return generateForwardMessageContent(message.forward, message.force);
|
|
215
|
+
if ('disappearingMessagesInChat' in message) {
|
|
216
|
+
const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
|
|
217
|
+
return prepareDisappearingMessageSettingContent(exp);
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleGroupInvite = async (message, options) => {
|
|
223
|
+
const m = {
|
|
224
|
+
groupInviteMessage: {
|
|
225
|
+
inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
|
|
226
|
+
caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (options.getProfilePicUrl) {
|
|
231
|
+
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
|
|
232
|
+
if (pfpUrl) {
|
|
233
|
+
const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
|
|
234
|
+
if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return m;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const handleEventMessage = (message, options) => {
|
|
241
|
+
const startTime = Math.floor(message.event.startDate.getTime() / 1000);
|
|
242
|
+
const m = {
|
|
243
|
+
eventMessage: {
|
|
244
|
+
name: message.event.name, description: message.event.description, startTime,
|
|
245
|
+
endTime: message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined,
|
|
246
|
+
isCanceled: message.event.isCancelled ?? false, extraGuestsAllowed: message.event.extraGuestsAllowed,
|
|
247
|
+
isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
|
|
248
|
+
},
|
|
249
|
+
messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (message.event.call && options.getCallLink) {
|
|
253
|
+
options.getCallLink(message.event.call, { startTime }).then(token => {
|
|
254
|
+
m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return m;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const handlePollMessage = (message) => {
|
|
261
|
+
message.poll.selectableCount ||= 0;
|
|
262
|
+
message.poll.toAnnouncementGroup ||= false;
|
|
263
|
+
|
|
264
|
+
if (!Array.isArray(message.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 });
|
|
265
|
+
if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length)
|
|
266
|
+
throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
|
|
267
|
+
|
|
268
|
+
const pollMsg = { name: message.poll.name, selectableOptionsCount: message.poll.selectableCount, options: message.poll.values.map(optionName => ({ optionName })) };
|
|
269
|
+
const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } };
|
|
270
|
+
if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg;
|
|
271
|
+
else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg;
|
|
272
|
+
else m.pollCreationMessage = pollMsg;
|
|
273
|
+
return m;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const handleProductMessage = async (message, options) => {
|
|
277
|
+
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
|
|
278
|
+
return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) };
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const handleRequestPayment = async (message, options) => {
|
|
282
|
+
const sticker = message.requestPayment.sticker ? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options) : null;
|
|
283
|
+
let notes = message.requestPayment.sticker
|
|
284
|
+
? { stickerMessage: { ...sticker.stickerMessage, contextInfo: message.requestPayment.contextInfo } }
|
|
285
|
+
: message.requestPayment.note ? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } } : null;
|
|
286
|
+
|
|
287
|
+
if (!notes) throw new Boom('Invalid request payment', { statusCode: 400 });
|
|
288
|
+
|
|
289
|
+
const m = {
|
|
290
|
+
requestPaymentMessage: WAProto.Message.RequestPaymentMessage.fromObject({
|
|
291
|
+
expiryTimestamp: message.requestPayment.expiryTimestamp || message.requestPayment.expiry,
|
|
292
|
+
amount1000: message.requestPayment.amount1000 || message.requestPayment.amount,
|
|
293
|
+
currencyCodeIso4217: message.requestPayment.currencyCodeIso4217 || message.requestPayment.currency,
|
|
294
|
+
requestFrom: message.requestPayment.requestFrom || message.requestPayment.from,
|
|
295
|
+
noteMessage: notes, background: message.requestPayment.background
|
|
296
|
+
})
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (message.requestPayment.currencyCodeIso4217 === 'BRL' && message.requestPayment.pixKey) {
|
|
300
|
+
if (!m.requestPaymentMessage.noteMessage.extendedTextMessage)
|
|
301
|
+
m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } };
|
|
302
|
+
m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${message.requestPayment.pixKey}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return m;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// ===== MAIN GENERATOR =====
|
|
309
|
+
export const generateWAMessageContent = async (message, options = {}) => {
|
|
310
|
+
const messageKeys = Object.keys(message);
|
|
311
|
+
|
|
312
|
+
// ===== SMART DETECTION =====
|
|
313
|
+
const isRawProtoMessage = messageKeys.some(key =>
|
|
314
|
+
key.endsWith('Message') &&
|
|
315
|
+
typeof message[key] === 'object' &&
|
|
316
|
+
!HIGH_LEVEL_KEYS.includes(key)
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message);
|
|
320
|
+
|
|
321
|
+
// Pass through raw protocol messages directly
|
|
322
|
+
if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) {
|
|
323
|
+
return WAProto.Message.create(message);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// If no high-level keys AND has proto message keys, pass through
|
|
327
|
+
if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) {
|
|
328
|
+
return WAProto.Message.create(message);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let m = {};
|
|
332
|
+
|
|
333
|
+
// ===== HANDLE TEXT =====
|
|
334
|
+
if ('text' in message && !('buttons' in message) && !('templateButtons' in message) && !('sections' in message) && !('interactiveButtons' in message) && !('shop' in message)) {
|
|
335
|
+
m = await handleTextMessage(message, options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ===== HANDLE SPECIAL MESSAGES =====
|
|
339
|
+
else {
|
|
340
|
+
const special = await handleSpecialMessages(message, options);
|
|
341
|
+
if (special) m = special;
|
|
342
|
+
else if ('groupInvite' in message) m = await handleGroupInvite(message, options);
|
|
343
|
+
else if ('stickerPack' in message) return await prepareStickerPackMessage(message.stickerPack, options);
|
|
344
|
+
else if ('pin' in message) {
|
|
345
|
+
const messageKey = typeof message.pin === 'boolean' ? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation'); })()) : (message.pin && typeof message.pin === 'object') ? (message.pin.key || message.pin.stanzaId || (message.pin.id ? { remoteJid: options.jid, fromMe: message.pin.fromMe || false, id: message.pin.id, participant: message.pin.participant || message.pin.sender } : null)) : message.pin;
|
|
346
|
+
const shouldPin = typeof message.pin === 'boolean' ? message.pin : (message.pin && typeof message.pin === 'object' ? message.pin.unpin !== true : true);
|
|
347
|
+
const pinTime = message.pin && typeof message.pin === 'object' ? message.pin.time : message.time;
|
|
348
|
+
if (!messageKey || !messageKey.id) throw new Boom('Invalid message key for pin operation');
|
|
349
|
+
m = { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (pinTime || 86400) : 0 } };
|
|
350
|
+
}
|
|
351
|
+
else if ('keep' in message) m = { keepInChatMessage: { key: message.keep, keepType: message.type, timestampMs: Date.now() } };
|
|
352
|
+
else if ('call' in message) m = { scheduledCallCreationMessage: { scheduledTimestampMs: message.call.time || Date.now(), callType: message.call.type || 1, title: message.call.title } };
|
|
353
|
+
else if ('paymentInvite' in message) m = { paymentInviteMessage: { serviceType: message.paymentInvite.type, expiryTimestamp: message.paymentInvite.expiry } };
|
|
354
|
+
else if ('buttonReply' in message) m = message.type === 'template' ? { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } } : { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: 0 } };
|
|
355
|
+
else if ('ptv' in message && message.ptv) {
|
|
356
|
+
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
|
|
357
|
+
m = { ptvMessage: videoMessage };
|
|
358
|
+
}
|
|
359
|
+
else if ('product' in message) m = await handleProductMessage(message, options);
|
|
360
|
+
else if ('order' in message) m = { orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: message.order.id, thumbnail: message.order.thumbnail, itemCount: message.order.itemCount, status: message.order.status, surface: message.order.surface, orderTitle: message.order.title, message: message.order.text, sellerJid: message.order.seller, token: message.order.token, totalAmount1000: message.order.amount, totalCurrencyCode: message.order.currency }) };
|
|
361
|
+
else if ('listReply' in message) m = { listResponseMessage: { ...message.listReply } };
|
|
362
|
+
else if ('event' in message) m = handleEventMessage(message, options);
|
|
363
|
+
else if ('poll' in message) m = handlePollMessage(message);
|
|
364
|
+
else if ('inviteAdmin' in message) m = { newsletterAdminInviteMessage: { inviteExpiration: message.inviteAdmin.inviteExpiration, caption: message.inviteAdmin.text, newsletterJid: message.inviteAdmin.jid, newsletterName: message.inviteAdmin.subject, jpegThumbnail: message.inviteAdmin.thumbnail } };
|
|
365
|
+
else if ('requestPayment' in message) m = await handleRequestPayment(message, options);
|
|
366
|
+
else if ('extendedTextMessage' in message) m = { extendedTextMessage: WAProto.Message.ExtendedTextMessage.create(message.extendedTextMessage) };
|
|
367
|
+
else if ('interactiveMessage' in message) m = { interactiveMessage: WAProto.Message.InteractiveMessage.create(message.interactiveMessage) };
|
|
368
|
+
else if ('sharePhoneNumber' in message) m = { protocolMessage: { type: 4 } };
|
|
369
|
+
else if ('requestPhoneNumber' in message) m = { requestPhoneNumberMessage: {} };
|
|
370
|
+
else if ('limitSharing' in message) m = { protocolMessage: { type: 3, limitSharing: { sharingLimited: message.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } };
|
|
371
|
+
else if ('album' in message) {
|
|
372
|
+
const imageMessages = message.album.filter(item => 'image' in item);
|
|
373
|
+
const videoMessages = message.album.filter(item => 'video' in item);
|
|
374
|
+
m = { albumMessage: { expectedImageCount: imageMessages.length, expectedVideoCount: videoMessages.length } };
|
|
375
|
+
}
|
|
376
|
+
else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ===== SMART BUTTON HANDLING =====
|
|
380
|
+
if ('buttons' in message && Array.isArray(message.buttons) && message.buttons.length > 0) {
|
|
381
|
+
const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson);
|
|
382
|
+
|
|
383
|
+
if (hasNativeFlow) {
|
|
384
|
+
// Convert to interactiveMessage
|
|
385
|
+
const interactive = {
|
|
386
|
+
body: { text: message.text || message.caption || message.contentText || '' },
|
|
387
|
+
footer: { text: message.footer || message.footerText || '' },
|
|
388
|
+
nativeFlowMessage: {
|
|
389
|
+
buttons: message.buttons.map(btn => {
|
|
390
|
+
if (btn.name && btn.buttonParamsJson) return btn;
|
|
391
|
+
if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson };
|
|
392
|
+
return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) };
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
|
|
398
|
+
if (Object.keys(m).length > 0) {
|
|
399
|
+
interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true };
|
|
400
|
+
Object.assign(interactive.header, m);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
m = { interactiveMessage: interactive };
|
|
404
|
+
} else {
|
|
405
|
+
// Old-style buttons
|
|
406
|
+
const buttonsMessage = { buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) };
|
|
407
|
+
if ('text' in message) { buttonsMessage.contentText = message.text; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY; }
|
|
408
|
+
else { if ('caption' in message) buttonsMessage.contentText = message.caption; const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase(); buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY; Object.assign(buttonsMessage, m); }
|
|
409
|
+
if (message.title) { buttonsMessage.text = message.title; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT; }
|
|
410
|
+
if (message.footer) buttonsMessage.footerText = message.footer;
|
|
411
|
+
m = { buttonsMessage };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ===== TEMPLATE BUTTONS =====
|
|
416
|
+
else if ('templateButtons' in message && !!message.templateButtons) {
|
|
417
|
+
const msg = { hydratedButtons: message.templateButtons };
|
|
418
|
+
if ('text' in message) msg.hydratedContentText = message.text;
|
|
419
|
+
else { if ('caption' in message) msg.hydratedContentText = message.caption; Object.assign(msg, m); }
|
|
420
|
+
if ('footer' in message && !!message.footer) msg.hydratedFooterText = message.footer;
|
|
421
|
+
m = { templateMessage: { fourRowTemplate: msg, hydratedTemplate: msg } };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ===== LIST MESSAGE =====
|
|
425
|
+
else if ('sections' in message && !!message.sections) {
|
|
426
|
+
m = { listMessage: { sections: message.sections, buttonText: message.buttonText, title: message.title, footerText: message.footer, description: message.text, listType: proto.Message.ListMessage.ListType.SINGLE_SELECT } };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ===== INTERACTIVE BUTTONS =====
|
|
430
|
+
else if ('interactiveButtons' in message && !!message.interactiveButtons) {
|
|
431
|
+
const interactiveMessage = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: message.interactiveButtons }) };
|
|
432
|
+
if ('text' in message) interactiveMessage.body = { text: message.text };
|
|
433
|
+
else if ('caption' in message) { interactiveMessage.body = { text: message.caption }; interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
|
|
434
|
+
if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
|
|
435
|
+
if ('title' in message && !!message.title) { interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
|
|
436
|
+
m = { interactiveMessage };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ===== SHOP MESSAGE (YOUR EXAMPLE) =====
|
|
440
|
+
else if ('shop' in message && !!message.shop) {
|
|
441
|
+
const interactiveMessage = {
|
|
442
|
+
shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({
|
|
443
|
+
surface: message.shop.surface || 1,
|
|
444
|
+
id: message.shop.id || message.id
|
|
445
|
+
})
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Handle body text
|
|
449
|
+
if ('text' in message) interactiveMessage.body = { text: message.text };
|
|
450
|
+
else if ('caption' in message) interactiveMessage.body = { text: message.caption };
|
|
451
|
+
|
|
452
|
+
// Handle header with media
|
|
453
|
+
if (message.title || message.subtitle || Object.keys(m).length > 0) {
|
|
454
|
+
interactiveMessage.header = {
|
|
455
|
+
title: message.title || '',
|
|
456
|
+
subtitle: message.subtitle || '',
|
|
457
|
+
hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0)
|
|
458
|
+
};
|
|
459
|
+
if (Object.keys(m).length > 0) Object.assign(interactiveMessage.header, m);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
|
|
463
|
+
|
|
464
|
+
m = { interactiveMessage };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ===== AUTO-APPLY CONTEXT & WRAPPERS =====
|
|
468
|
+
const finalKey = Object.keys(m)[0];
|
|
469
|
+
|
|
470
|
+
// Auto-merge contextInfo and mentions
|
|
471
|
+
if ((message.contextInfo || message.mentions) && finalKey && m[finalKey]) {
|
|
472
|
+
m[finalKey].contextInfo = {
|
|
473
|
+
...(m[finalKey].contextInfo || {}),
|
|
474
|
+
...(message.contextInfo || {}),
|
|
475
|
+
mentionedJid: message.mentions || message.contextInfo?.mentionedJid || []
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ViewOnce wrapper
|
|
480
|
+
if (message.viewOnce === true) m = { viewOnceMessage: { message: m } };
|
|
481
|
+
|
|
482
|
+
// Edit wrapper
|
|
483
|
+
if (message.edit) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } };
|
|
484
|
+
|
|
485
|
+
return WAProto.Message.create(m);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
export const generateWAMessageFromContent = (jid, message, options) => {
|
|
490
|
+
if (!options.timestamp) options.timestamp = new Date();
|
|
491
|
+
const innerMessage = normalizeMessageContent(message);
|
|
492
|
+
const key = getContentType(innerMessage);
|
|
493
|
+
const timestamp = unixTimestampSeconds(options.timestamp);
|
|
494
|
+
const { quoted, userJid } = options;
|
|
495
|
+
|
|
496
|
+
if (quoted && !isJidNewsletter(jid)) {
|
|
497
|
+
const participant = quoted.key.fromMe ? userJid : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
|
|
498
|
+
const quotedMsg = proto.Message.create({ [getContentType(normalizeMessageContent(quoted.message))]: normalizeMessageContent(quoted.message)[getContentType(normalizeMessageContent(quoted.message))] });
|
|
499
|
+
const contextInfo = (innerMessage[key]?.contextInfo) || {};
|
|
500
|
+
contextInfo.participant = jidNormalizedUser(participant);
|
|
501
|
+
contextInfo.stanzaId = quoted.key.id;
|
|
502
|
+
contextInfo.quotedMessage = quotedMsg;
|
|
503
|
+
if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid;
|
|
504
|
+
innerMessage[key].contextInfo = contextInfo;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
|
|
508
|
+
innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return WAProto.WebMessageInfo.fromObject({
|
|
512
|
+
key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2(), participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined },
|
|
513
|
+
message: WAProto.Message.create(message),
|
|
514
|
+
messageTimestamp: timestamp,
|
|
515
|
+
messageStubParameters: [],
|
|
516
|
+
participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
|
|
517
|
+
status: WAMessageStatus.PENDING
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
export const generateWAMessage = async (jid, content, options = {}) => {
|
|
522
|
+
options.logger = options?.logger?.child({ msgId: options.messageId });
|
|
523
|
+
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// ===== UTILITIES =====
|
|
527
|
+
export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown';
|
|
528
|
+
|
|
529
|
+
export const updateMessageWithReceipt = (msg, receipt) => {
|
|
530
|
+
msg.userReceipt ||= [];
|
|
531
|
+
const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
|
|
532
|
+
if (recp) Object.assign(recp, receipt);
|
|
533
|
+
else msg.userReceipt.push(receipt);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
export const updateMessageWithReaction = (msg, reaction) => {
|
|
537
|
+
const authorID = getKeyAuthor(reaction.key);
|
|
538
|
+
msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
|
|
539
|
+
reaction.text ||= '';
|
|
540
|
+
msg.reactions.push(reaction);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
export const updateMessageWithPollUpdate = (msg, update) => {
|
|
544
|
+
const authorID = getKeyAuthor(update.pollUpdateMessageKey);
|
|
545
|
+
msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
|
|
546
|
+
if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update);
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
|
|
550
|
+
const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || [];
|
|
551
|
+
const voteHashMap = opts.reduce((acc, opt) => {
|
|
552
|
+
const hash = sha256(Buffer.from(opt.optionName || '')).toString();
|
|
553
|
+
acc[hash] = { name: opt.optionName || '', voters: [] };
|
|
554
|
+
return acc;
|
|
555
|
+
}, {});
|
|
556
|
+
|
|
557
|
+
for (const update of pollUpdates || []) {
|
|
558
|
+
const { vote } = update;
|
|
559
|
+
if (!vote) continue;
|
|
560
|
+
for (const option of vote.selectedOptions || []) {
|
|
561
|
+
const hash = option.toString();
|
|
562
|
+
voteHashMap[hash] ||= { name: 'Unknown', voters: [] };
|
|
563
|
+
voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return Object.values(voteHashMap);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export const aggregateMessageKeysNotFromMe = (keys) => {
|
|
570
|
+
const keyMap = {};
|
|
571
|
+
for (const { remoteJid, id, participant, fromMe } of keys) {
|
|
572
|
+
if (!fromMe) {
|
|
573
|
+
const uqKey = `${remoteJid}:${participant || ''}`;
|
|
574
|
+
keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] };
|
|
575
|
+
keyMap[uqKey].messageIds.push(id);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return Object.values(keyMap);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const REUPLOAD_STATUS = [410, 404];
|
|
582
|
+
|
|
583
|
+
export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
584
|
+
const downloadMsg = async () => {
|
|
585
|
+
let normalizedMessage = message;
|
|
586
|
+
if (!message.message && message.key && message.participant) normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
|
|
587
|
+
if (!normalizedMessage.message && typeof message === 'object') {
|
|
588
|
+
const possibleMessage = message.message || message.quoted?.message || message;
|
|
589
|
+
normalizedMessage = { key: message.key || {}, message: possibleMessage, messageTimestamp: message.messageTimestamp };
|
|
590
|
+
}
|
|
591
|
+
const mContent = extractMessageContent(normalizedMessage.message);
|
|
592
|
+
if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message });
|
|
593
|
+
const contentType = getContentType(mContent);
|
|
594
|
+
let mediaType = contentType?.replace('Message', '');
|
|
595
|
+
const media = mContent[contentType];
|
|
596
|
+
if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media)))
|
|
597
|
+
throw new Boom(`"${contentType}" is not a media message`);
|
|
598
|
+
const download = 'thumbnailDirectPath' in media && !('url' in media) ? { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey } : media;
|
|
599
|
+
const stream = await downloadContentFromMessage(download, mediaType, options);
|
|
600
|
+
if (type === 'buffer') {
|
|
601
|
+
const chunks = [];
|
|
602
|
+
for await (const chunk of stream) chunks.push(chunk);
|
|
603
|
+
return Buffer.concat(chunks);
|
|
604
|
+
}
|
|
605
|
+
return stream;
|
|
606
|
+
};
|
|
607
|
+
return downloadMsg().catch(async (error) => {
|
|
608
|
+
if (ctx && typeof error?.status === 'number' && REUPLOAD_STATUS.includes(error.status)) {
|
|
609
|
+
message = await ctx.reuploadRequest(message);
|
|
610
|
+
return downloadMsg();
|
|
611
|
+
}
|
|
612
|
+
throw error;
|
|
613
|
+
});
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
export async function prepareStickerPackMessage(stickerPack, options) {
|
|
617
|
+
const { stickers, name, publisher, packId, description } = stickerPack;
|
|
618
|
+
if (!stickers?.length) throw new Boom('Sticker pack requires at least one sticker', { statusCode: 400 });
|
|
619
|
+
|
|
620
|
+
const lib = await getImageProcessingLibrary();
|
|
621
|
+
const packId_ = packId || generateMessageIDV2();
|
|
622
|
+
const validStickers = [];
|
|
623
|
+
|
|
624
|
+
for (const s of stickers) {
|
|
625
|
+
try {
|
|
626
|
+
const { stream } = await getStream(s.data);
|
|
627
|
+
let buffer = await toBuffer(stream);
|
|
628
|
+
const isWebP = buffer.length >= 12 && buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46;
|
|
629
|
+
if (!isWebP) {
|
|
630
|
+
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp().toBuffer();
|
|
631
|
+
else if ('jimp' in lib) buffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'));
|
|
632
|
+
}
|
|
633
|
+
if (buffer.length > 1024 * 1024) {
|
|
634
|
+
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer();
|
|
635
|
+
if (buffer.length > 1024 * 1024) continue;
|
|
636
|
+
}
|
|
637
|
+
validStickers.push({
|
|
638
|
+
fileName: `${sha256(buffer).toString('base64').replace(/\//g, '-')}.webp`,
|
|
639
|
+
buffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false,
|
|
640
|
+
emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel
|
|
641
|
+
});
|
|
642
|
+
} catch (e) { options.logger?.warn(`Sticker failed: ${e.message}`); }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!validStickers.length) throw new Boom('No valid stickers', { statusCode: 400 });
|
|
646
|
+
|
|
647
|
+
const { stream: covStream } = await getStream(stickerPack.cover);
|
|
648
|
+
let coverBuffer = await toBuffer(covStream);
|
|
649
|
+
const isWebPCover = coverBuffer.length >= 12 && coverBuffer[0] === 0x52 && coverBuffer[1] === 0x49 && coverBuffer[2] === 0x46 && coverBuffer[3] === 0x46;
|
|
650
|
+
if (!isWebPCover) {
|
|
651
|
+
if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
|
|
652
|
+
else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const processBatch = async (batch, batchIdx) => {
|
|
656
|
+
const batchData = {};
|
|
657
|
+
batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
|
|
658
|
+
const trayFile = `${packId_}_batch${batchIdx}.webp`;
|
|
659
|
+
batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
|
|
660
|
+
|
|
661
|
+
const zipBuf = await new Promise((resolve, reject) => { zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))); });
|
|
662
|
+
const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
|
|
663
|
+
const uploadRes = await options.upload(upload.encFilePath, {
|
|
664
|
+
fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
665
|
+
});
|
|
666
|
+
await fs.unlink(upload.encFilePath);
|
|
667
|
+
|
|
668
|
+
let thumbBuf;
|
|
669
|
+
if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
|
|
670
|
+
else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
|
|
671
|
+
|
|
672
|
+
let thumbUploadRes;
|
|
673
|
+
if (thumbBuf?.length) {
|
|
674
|
+
const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
|
|
675
|
+
thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
|
|
676
|
+
fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
677
|
+
});
|
|
678
|
+
await fs.unlink(thumbUpload.encFilePath);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
name: `${name} (${batchIdx + 1})`, publisher, packDescription: description, stickerPackId: `${packId_}_${batchIdx}`,
|
|
683
|
+
stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
|
|
684
|
+
stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
|
|
685
|
+
fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
|
|
686
|
+
directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
|
|
687
|
+
...(thumbUploadRes && { thumbnailDirectPath: thumbUploadRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined })
|
|
688
|
+
};
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
if (validStickers.length > 60) {
|
|
692
|
+
const batches = [];
|
|
693
|
+
for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
|
|
694
|
+
const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
|
|
695
|
+
return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export const assertMediaContent = (content) => {
|
|
702
|
+
content = extractMessageContent(content);
|
|
703
|
+
const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage;
|
|
704
|
+
if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content });
|
|
705
|
+
return mediaContent;
|
|
706
706
|
};
|