@nexustechpro/baileys 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +306 -261
- package/WAProto/fix-imports.js +71 -19
- package/WAProto/index.js +65468 -137440
- package/engine-requirements.js +10 -0
- package/lib/Defaults/index.js +145 -114
- package/lib/Socket/Client/websocket.js +35 -131
- package/lib/Socket/communities.js +1 -1
- package/lib/Socket/messages-recv.js +364 -1003
- package/lib/Socket/messages-send.js +179 -576
- package/lib/Socket/newsletter.js +141 -293
- package/lib/Socket/nexus-handler.js +29 -7
- package/lib/Socket/socket.js +162 -644
- package/lib/Store/index.js +5 -5
- package/lib/Store/make-in-memory-store.js +2 -2
- package/lib/Types/Newsletter.js +29 -31
- package/lib/Utils/chat-utils.js +1 -1
- package/lib/Utils/crypto.js +21 -47
- package/lib/Utils/generics.js +269 -352
- package/lib/Utils/identity-chnage-handler.js +55 -0
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/lt-hash.js +2 -2
- package/lib/Utils/messages-media.js +200 -15
- package/lib/Utils/messages.js +354 -287
- package/lib/Utils/noise-handler.js +200 -166
- package/lib/WABinary/generic-utils.js +94 -0
- package/lib/index.js +2 -2
- package/package.json +137 -120
package/lib/Utils/messages.js
CHANGED
|
@@ -8,11 +8,15 @@ import { WAMessageStatus, WAProto } from '../Types/index.js';
|
|
|
8
8
|
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
9
|
import { sha256 } from './crypto.js';
|
|
10
10
|
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
|
|
11
|
-
import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
|
|
11
|
+
import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
|
|
12
12
|
|
|
13
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
14
|
const MessageTypeProto = { image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage, audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage, document: WAProto.Message.DocumentMessage };
|
|
15
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 =====
|
|
16
20
|
export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
|
|
17
21
|
|
|
18
22
|
export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
|
|
@@ -28,112 +32,133 @@ const assertColor = (color) => {
|
|
|
28
32
|
return parseInt((hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex), 16);
|
|
29
33
|
};
|
|
30
34
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
opts: options.options
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const [{ mediaUrl, directPath }] = await Promise.all([
|
|
40
|
-
options.upload(encFilePath, {
|
|
41
|
-
fileEncSha256B64: fileEncSha256.toString('base64'),
|
|
42
|
-
mediaType,
|
|
43
|
-
timeoutMs: options.mediaUploadTimeoutMs
|
|
44
|
-
}),
|
|
45
|
-
(async () => {
|
|
46
|
-
try {
|
|
47
|
-
if (['image', 'video'].includes(mediaType) && !uploadData.jpegThumbnail) {
|
|
48
|
-
const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
|
|
49
|
-
uploadData.jpegThumbnail = thumbnail;
|
|
50
|
-
if (originalImageDimensions && !uploadData.width) {
|
|
51
|
-
uploadData.width = originalImageDimensions.width;
|
|
52
|
-
uploadData.height = originalImageDimensions.height;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (mediaType === 'audio' && !uploadData.seconds)
|
|
56
|
-
uploadData.seconds = await getAudioDuration(originalFilePath);
|
|
57
|
-
if (mediaType === 'audio' && uploadData.ptt)
|
|
58
|
-
uploadData.waveform = await getAudioWaveform(originalFilePath, options.logger);
|
|
59
|
-
if (options.backgroundColor && mediaType === 'audio' && uploadData.ptt)
|
|
60
|
-
uploadData.backgroundArgb = assertColor(options.backgroundColor);
|
|
61
|
-
} catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info'); }
|
|
62
|
-
})()
|
|
63
|
-
]).finally(async () => {
|
|
64
|
-
try {
|
|
65
|
-
await fs.unlink(encFilePath);
|
|
66
|
-
if (originalFilePath) await fs.unlink(originalFilePath);
|
|
67
|
-
} catch { }
|
|
68
|
-
});
|
|
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
|
+
};
|
|
69
40
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
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
|
+
};
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
55
|
};
|
|
82
56
|
|
|
57
|
+
// ===== MEDIA PREPARATION =====
|
|
83
58
|
export const prepareWAMessageMedia = async (message, options) => {
|
|
84
|
-
let mediaType = MEDIA_KEYS.find(key => key in message)
|
|
85
|
-
if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
|
|
59
|
+
let mediaType = MEDIA_KEYS.find(key => key in message)
|
|
60
|
+
if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
|
|
86
61
|
|
|
87
|
-
const uploadData = { ...message, media: message[mediaType] }
|
|
88
|
-
delete uploadData[mediaType]
|
|
62
|
+
const uploadData = { ...message, media: message[mediaType] }
|
|
63
|
+
delete uploadData[mediaType]
|
|
89
64
|
|
|
90
|
-
const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache
|
|
91
|
-
? `${mediaType}:${uploadData.media.url.toString()}` : null;
|
|
65
|
+
const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache ? `${mediaType}:${uploadData.media.url.toString()}` : null
|
|
92
66
|
|
|
93
|
-
if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
|
|
94
|
-
if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
|
67
|
+
if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
|
|
68
|
+
if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
|
95
69
|
|
|
96
70
|
if (cacheableKey) {
|
|
97
|
-
const cached = await options.mediaCache?.get(cacheableKey)
|
|
71
|
+
const cached = await options.mediaCache?.get(cacheableKey)
|
|
98
72
|
if (cached) {
|
|
99
|
-
const obj = proto.Message.decode(cached)
|
|
100
|
-
Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
|
|
101
|
-
return obj
|
|
73
|
+
const obj = proto.Message.decode(cached)
|
|
74
|
+
Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
|
|
75
|
+
return obj
|
|
102
76
|
}
|
|
103
77
|
}
|
|
104
78
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
mediaType, timeoutMs: options.mediaUploadTimeoutMs
|
|
110
|
-
});
|
|
111
|
-
await fs.unlink(filePath);
|
|
112
|
-
const obj = WAProto.Message.fromObject({
|
|
113
|
-
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
|
|
114
|
-
url: mediaUrl, directPath, fileSha256, fileLength, ...uploadData, media: undefined
|
|
115
|
-
})
|
|
116
|
-
});
|
|
117
|
-
if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage; }
|
|
118
|
-
if (obj.stickerMessage) obj.stickerMessage.stickerSentTs = Date.now();
|
|
119
|
-
if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish());
|
|
120
|
-
return obj;
|
|
121
|
-
}
|
|
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
|
|
122
83
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
125
95
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|
|
133
118
|
}
|
|
134
|
-
|
|
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 {}
|
|
135
138
|
}
|
|
136
|
-
|
|
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
|
+
});
|
|
137
162
|
|
|
138
163
|
export const generateForwardMessageContent = (message, forceForward) => {
|
|
139
164
|
const content = proto.Message.decode(proto.Message.encode(normalizeMessageContent(message.message)).finish());
|
|
@@ -150,33 +175,26 @@ export const generateForwardMessageContent = (message, forceForward) => {
|
|
|
150
175
|
return content;
|
|
151
176
|
};
|
|
152
177
|
|
|
178
|
+
// ===== MESSAGE HANDLERS =====
|
|
153
179
|
const handleTextMessage = async (message, options) => {
|
|
154
180
|
const extContent = { text: message.text };
|
|
155
181
|
let urlInfo = message.linkPreview || await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
|
|
156
182
|
|
|
157
183
|
if (urlInfo) {
|
|
158
184
|
Object.assign(extContent, {
|
|
159
|
-
matchedText: urlInfo['matched-text'],
|
|
160
|
-
|
|
161
|
-
description: urlInfo.description,
|
|
162
|
-
title: urlInfo.title,
|
|
163
|
-
previewType: 0
|
|
185
|
+
matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
|
|
186
|
+
description: urlInfo.description, title: urlInfo.title, previewType: 0
|
|
164
187
|
});
|
|
165
188
|
if (urlInfo.highQualityThumbnail) {
|
|
166
189
|
const img = urlInfo.highQualityThumbnail;
|
|
167
190
|
Object.assign(extContent, {
|
|
168
|
-
thumbnailDirectPath: img.directPath,
|
|
169
|
-
|
|
170
|
-
mediaKeyTimestamp: img.mediaKeyTimestamp,
|
|
171
|
-
thumbnailWidth: img.width,
|
|
172
|
-
thumbnailHeight: img.height,
|
|
173
|
-
thumbnailSha256: img.fileSha256,
|
|
174
|
-
thumbnailEncSha256: img.fileEncSha256
|
|
191
|
+
thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
|
|
192
|
+
thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
|
|
175
193
|
});
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
|
|
179
|
-
if (options.backgroundColor) extContent.backgroundArgb =
|
|
197
|
+
if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
|
|
180
198
|
if (options.font) extContent.font = options.font;
|
|
181
199
|
return { extendedTextMessage: extContent };
|
|
182
200
|
};
|
|
@@ -185,9 +203,7 @@ const handleSpecialMessages = async (message, options) => {
|
|
|
185
203
|
if ('contacts' in message) {
|
|
186
204
|
const { contacts } = message.contacts;
|
|
187
205
|
if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 });
|
|
188
|
-
return contacts.length === 1
|
|
189
|
-
? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) }
|
|
190
|
-
: { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
|
|
206
|
+
return contacts.length === 1 ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) } : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
|
|
191
207
|
}
|
|
192
208
|
if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
|
|
193
209
|
if ('react' in message) {
|
|
@@ -197,9 +213,7 @@ const handleSpecialMessages = async (message, options) => {
|
|
|
197
213
|
if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } };
|
|
198
214
|
if ('forward' in message) return generateForwardMessageContent(message.forward, message.force);
|
|
199
215
|
if ('disappearingMessagesInChat' in message) {
|
|
200
|
-
const exp = typeof message.disappearingMessagesInChat === 'boolean'
|
|
201
|
-
? message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0
|
|
202
|
-
: message.disappearingMessagesInChat;
|
|
216
|
+
const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
|
|
203
217
|
return prepareDisappearingMessageSettingContent(exp);
|
|
204
218
|
}
|
|
205
219
|
return null;
|
|
@@ -208,11 +222,8 @@ const handleSpecialMessages = async (message, options) => {
|
|
|
208
222
|
const handleGroupInvite = async (message, options) => {
|
|
209
223
|
const m = {
|
|
210
224
|
groupInviteMessage: {
|
|
211
|
-
inviteCode: message.groupInvite.inviteCode,
|
|
212
|
-
|
|
213
|
-
caption: message.groupInvite.text,
|
|
214
|
-
groupJid: message.groupInvite.jid,
|
|
215
|
-
groupName: message.groupInvite.subject
|
|
225
|
+
inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
|
|
226
|
+
caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
|
|
216
227
|
}
|
|
217
228
|
};
|
|
218
229
|
|
|
@@ -230,14 +241,10 @@ const handleEventMessage = (message, options) => {
|
|
|
230
241
|
const startTime = Math.floor(message.event.startDate.getTime() / 1000);
|
|
231
242
|
const m = {
|
|
232
243
|
eventMessage: {
|
|
233
|
-
name: message.event.name,
|
|
234
|
-
description: message.event.description,
|
|
235
|
-
startTime,
|
|
244
|
+
name: message.event.name, description: message.event.description, startTime,
|
|
236
245
|
endTime: message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined,
|
|
237
|
-
isCanceled: message.event.isCancelled ?? false,
|
|
238
|
-
|
|
239
|
-
isScheduleCall: message.event.isScheduleCall ?? false,
|
|
240
|
-
location: message.event.location
|
|
246
|
+
isCanceled: message.event.isCancelled ?? false, extraGuestsAllowed: message.event.extraGuestsAllowed,
|
|
247
|
+
isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
|
|
241
248
|
},
|
|
242
249
|
messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
|
|
243
250
|
};
|
|
@@ -258,12 +265,7 @@ const handlePollMessage = (message) => {
|
|
|
258
265
|
if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length)
|
|
259
266
|
throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
|
|
260
267
|
|
|
261
|
-
const pollMsg = {
|
|
262
|
-
name: message.poll.name,
|
|
263
|
-
selectableOptionsCount: message.poll.selectableCount,
|
|
264
|
-
options: message.poll.values.map(optionName => ({ optionName }))
|
|
265
|
-
};
|
|
266
|
-
|
|
268
|
+
const pollMsg = { name: message.poll.name, selectableOptionsCount: message.poll.selectableCount, options: message.poll.values.map(optionName => ({ optionName })) };
|
|
267
269
|
const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } };
|
|
268
270
|
if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg;
|
|
269
271
|
else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg;
|
|
@@ -273,24 +275,14 @@ const handlePollMessage = (message) => {
|
|
|
273
275
|
|
|
274
276
|
const handleProductMessage = async (message, options) => {
|
|
275
277
|
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
|
|
276
|
-
return {
|
|
277
|
-
productMessage: WAProto.Message.ProductMessage.create({
|
|
278
|
-
...message,
|
|
279
|
-
product: { ...message.product, productImage: imageMessage }
|
|
280
|
-
})
|
|
281
|
-
};
|
|
278
|
+
return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) };
|
|
282
279
|
};
|
|
283
280
|
|
|
284
281
|
const handleRequestPayment = async (message, options) => {
|
|
285
|
-
const sticker = message.requestPayment.sticker
|
|
286
|
-
? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options)
|
|
287
|
-
: null;
|
|
288
|
-
|
|
282
|
+
const sticker = message.requestPayment.sticker ? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options) : null;
|
|
289
283
|
let notes = message.requestPayment.sticker
|
|
290
284
|
? { stickerMessage: { ...sticker.stickerMessage, contextInfo: message.requestPayment.contextInfo } }
|
|
291
|
-
: message.requestPayment.note
|
|
292
|
-
? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } }
|
|
293
|
-
: null;
|
|
285
|
+
: message.requestPayment.note ? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } } : null;
|
|
294
286
|
|
|
295
287
|
if (!notes) throw new Boom('Invalid request payment', { statusCode: 400 });
|
|
296
288
|
|
|
@@ -300,8 +292,7 @@ const handleRequestPayment = async (message, options) => {
|
|
|
300
292
|
amount1000: message.requestPayment.amount1000 || message.requestPayment.amount,
|
|
301
293
|
currencyCodeIso4217: message.requestPayment.currencyCodeIso4217 || message.requestPayment.currency,
|
|
302
294
|
requestFrom: message.requestPayment.requestFrom || message.requestPayment.from,
|
|
303
|
-
noteMessage: notes,
|
|
304
|
-
background: message.requestPayment.background
|
|
295
|
+
noteMessage: notes, background: message.requestPayment.background
|
|
305
296
|
})
|
|
306
297
|
};
|
|
307
298
|
|
|
@@ -314,90 +305,199 @@ const handleRequestPayment = async (message, options) => {
|
|
|
314
305
|
return m;
|
|
315
306
|
};
|
|
316
307
|
|
|
317
|
-
|
|
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
|
+
|
|
318
331
|
let m = {};
|
|
319
332
|
|
|
320
|
-
|
|
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 =====
|
|
321
339
|
else {
|
|
322
340
|
const special = await handleSpecialMessages(message, options);
|
|
323
341
|
if (special) m = special;
|
|
324
342
|
else if ('groupInvite' in message) m = await handleGroupInvite(message, options);
|
|
325
343
|
else if ('stickerPack' in message) return await prepareStickerPackMessage(message.stickerPack, options);
|
|
326
|
-
else if ('pin' in message)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
m =
|
|
332
|
-
? { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } }
|
|
333
|
-
: { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: 0 } };
|
|
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 } };
|
|
334
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 } };
|
|
335
355
|
else if ('ptv' in message && message.ptv) {
|
|
336
356
|
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
|
|
337
357
|
m = { ptvMessage: videoMessage };
|
|
338
358
|
}
|
|
339
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 }) };
|
|
340
361
|
else if ('listReply' in message) m = { listResponseMessage: { ...message.listReply } };
|
|
341
362
|
else if ('event' in message) m = handleEventMessage(message, options);
|
|
342
363
|
else if ('poll' in message) m = handlePollMessage(message);
|
|
343
|
-
else if ('inviteAdmin' in message) m = {
|
|
344
|
-
newsletterAdminInviteMessage: {
|
|
345
|
-
inviteExpiration: message.inviteAdmin.inviteExpiration,
|
|
346
|
-
caption: message.inviteAdmin.text,
|
|
347
|
-
newsletterJid: message.inviteAdmin.jid,
|
|
348
|
-
newsletterName: message.inviteAdmin.subject,
|
|
349
|
-
jpegThumbnail: message.inviteAdmin.thumbnail
|
|
350
|
-
}
|
|
351
|
-
};
|
|
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 } };
|
|
352
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) };
|
|
353
368
|
else if ('sharePhoneNumber' in message) m = { protocolMessage: { type: 4 } };
|
|
354
369
|
else if ('requestPhoneNumber' in message) m = { requestPhoneNumberMessage: {} };
|
|
355
|
-
else if ('limitSharing' in message) m = {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
})
|
|
363
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);
|
|
364
401
|
}
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
}
|
|
367
413
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if ('
|
|
371
|
-
const
|
|
372
|
-
if (
|
|
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 } };
|
|
373
422
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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);
|
|
380
460
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
461
|
+
|
|
462
|
+
if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
|
|
463
|
+
|
|
464
|
+
m = { interactiveMessage };
|
|
385
465
|
}
|
|
386
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
|
+
|
|
387
485
|
return WAProto.Message.create(m);
|
|
388
486
|
};
|
|
389
487
|
|
|
390
|
-
|
|
488
|
+
|
|
489
|
+
export const generateWAMessageFromContent = (jid, message, options = {}) => {
|
|
391
490
|
if (!options.timestamp) options.timestamp = new Date();
|
|
392
491
|
const innerMessage = normalizeMessageContent(message);
|
|
393
492
|
const key = getContentType(innerMessage);
|
|
394
493
|
const timestamp = unixTimestampSeconds(options.timestamp);
|
|
395
494
|
const { quoted, userJid } = options;
|
|
396
495
|
|
|
397
|
-
if (quoted && !isJidNewsletter(jid)) {
|
|
496
|
+
if (quoted && key && !isJidNewsletter(jid)) {
|
|
398
497
|
const participant = quoted.key.fromMe ? userJid : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
|
|
399
498
|
const quotedMsg = proto.Message.create({ [getContentType(normalizeMessageContent(quoted.message))]: normalizeMessageContent(quoted.message)[getContentType(normalizeMessageContent(quoted.message))] });
|
|
400
|
-
|
|
499
|
+
if (!innerMessage[key]) innerMessage[key] = {};
|
|
500
|
+
const contextInfo = innerMessage[key].contextInfo || {};
|
|
401
501
|
contextInfo.participant = jidNormalizedUser(participant);
|
|
402
502
|
contextInfo.stanzaId = quoted.key.id;
|
|
403
503
|
contextInfo.quotedMessage = quotedMsg;
|
|
@@ -405,12 +505,27 @@ export const generateWAMessageFromContent = (jid, message, options) => {
|
|
|
405
505
|
innerMessage[key].contextInfo = contextInfo;
|
|
406
506
|
}
|
|
407
507
|
|
|
408
|
-
if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
|
|
508
|
+
if (options?.ephemeralExpiration && key && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
|
|
509
|
+
if (!innerMessage[key]) innerMessage[key] = {};
|
|
409
510
|
innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
|
|
410
511
|
}
|
|
411
512
|
|
|
513
|
+
const findMessageKey = (obj) => {
|
|
514
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
515
|
+
if (obj.key?.id) return obj.key.id;
|
|
516
|
+
for (const value of Object.values(obj)) {
|
|
517
|
+
if (value && typeof value === 'object') {
|
|
518
|
+
const found = findMessageKey(value);
|
|
519
|
+
if (found) return found;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const messageId = findMessageKey(innerMessage) || options?.messageId || generateMessageIDV2();
|
|
526
|
+
|
|
412
527
|
return WAProto.WebMessageInfo.fromObject({
|
|
413
|
-
key: { remoteJid: jid, fromMe: true, id:
|
|
528
|
+
key: { remoteJid: jid, fromMe: true, id: messageId },
|
|
414
529
|
message: WAProto.Message.create(message),
|
|
415
530
|
messageTimestamp: timestamp,
|
|
416
531
|
messageStubParameters: [],
|
|
@@ -419,34 +534,12 @@ export const generateWAMessageFromContent = (jid, message, options) => {
|
|
|
419
534
|
});
|
|
420
535
|
};
|
|
421
536
|
|
|
422
|
-
export const generateWAMessage = async (jid, content, options) => {
|
|
537
|
+
export const generateWAMessage = async (jid, content, options = {}) => {
|
|
423
538
|
options.logger = options?.logger?.child({ msgId: options.messageId });
|
|
424
539
|
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
|
|
425
540
|
};
|
|
426
541
|
|
|
427
|
-
|
|
428
|
-
if (!content) return;
|
|
429
|
-
const keys = Object.keys(content);
|
|
430
|
-
return keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
export const normalizeMessageContent = (content) => {
|
|
434
|
-
if (!content) return;
|
|
435
|
-
for (let i = 0; i < 5; i++) {
|
|
436
|
-
const inner = content?.ephemeralMessage || content?.viewOnceMessage || content?.documentWithCaptionMessage || content?.viewOnceMessageV2 || content?.viewOnceMessageV2Extension || content?.editedMessage;
|
|
437
|
-
if (!inner) break;
|
|
438
|
-
content = inner.message;
|
|
439
|
-
}
|
|
440
|
-
return content;
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
export const extractMessageContent = (content) => {
|
|
444
|
-
content = normalizeMessageContent(content);
|
|
445
|
-
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 || '' };
|
|
446
|
-
|
|
447
|
-
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;
|
|
448
|
-
};
|
|
449
|
-
|
|
542
|
+
// ===== UTILITIES =====
|
|
450
543
|
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';
|
|
451
544
|
|
|
452
545
|
export const updateMessageWithReceipt = (msg, receipt) => {
|
|
@@ -506,9 +599,7 @@ const REUPLOAD_STATUS = [410, 404];
|
|
|
506
599
|
export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
507
600
|
const downloadMsg = async () => {
|
|
508
601
|
let normalizedMessage = message;
|
|
509
|
-
if (!message.message && message.key && message.participant) {
|
|
510
|
-
normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
|
|
511
|
-
}
|
|
602
|
+
if (!message.message && message.key && message.participant) normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
|
|
512
603
|
if (!normalizedMessage.message && typeof message === 'object') {
|
|
513
604
|
const possibleMessage = message.message || message.quoted?.message || message;
|
|
514
605
|
normalizedMessage = { key: message.key || {}, message: possibleMessage, messageTimestamp: message.messageTimestamp };
|
|
@@ -524,9 +615,7 @@ export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
|
524
615
|
const stream = await downloadContentFromMessage(download, mediaType, options);
|
|
525
616
|
if (type === 'buffer') {
|
|
526
617
|
const chunks = [];
|
|
527
|
-
for await (const chunk of stream)
|
|
528
|
-
chunks.push(chunk);
|
|
529
|
-
}
|
|
618
|
+
for await (const chunk of stream) chunks.push(chunk);
|
|
530
619
|
return Buffer.concat(chunks);
|
|
531
620
|
}
|
|
532
621
|
return stream;
|
|
@@ -553,24 +642,18 @@ export async function prepareStickerPackMessage(stickerPack, options) {
|
|
|
553
642
|
const { stream } = await getStream(s.data);
|
|
554
643
|
let buffer = await toBuffer(stream);
|
|
555
644
|
const isWebP = buffer.length >= 12 && buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46;
|
|
556
|
-
|
|
557
645
|
if (!isWebP) {
|
|
558
646
|
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp().toBuffer();
|
|
559
647
|
else if ('jimp' in lib) buffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'));
|
|
560
648
|
}
|
|
561
|
-
|
|
562
649
|
if (buffer.length > 1024 * 1024) {
|
|
563
650
|
if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer();
|
|
564
651
|
if (buffer.length > 1024 * 1024) continue;
|
|
565
652
|
}
|
|
566
|
-
|
|
567
653
|
validStickers.push({
|
|
568
654
|
fileName: `${sha256(buffer).toString('base64').replace(/\//g, '-')}.webp`,
|
|
569
|
-
buffer,
|
|
570
|
-
|
|
571
|
-
isAnimated: s.isAnimated || false,
|
|
572
|
-
emojis: s.emojis || [],
|
|
573
|
-
accessibilityLabel: s.accessibilityLabel
|
|
655
|
+
buffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false,
|
|
656
|
+
emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel
|
|
574
657
|
});
|
|
575
658
|
} catch (e) { options.logger?.warn(`Sticker failed: ${e.message}`); }
|
|
576
659
|
}
|
|
@@ -580,70 +663,55 @@ export async function prepareStickerPackMessage(stickerPack, options) {
|
|
|
580
663
|
const { stream: covStream } = await getStream(stickerPack.cover);
|
|
581
664
|
let coverBuffer = await toBuffer(covStream);
|
|
582
665
|
const isWebPCover = coverBuffer.length >= 12 && coverBuffer[0] === 0x52 && coverBuffer[1] === 0x49 && coverBuffer[2] === 0x46 && coverBuffer[3] === 0x46;
|
|
583
|
-
if (!isWebPCover) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
666
|
+
if (!isWebPCover) {
|
|
667
|
+
if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
|
|
668
|
+
else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
|
|
669
|
+
}
|
|
587
670
|
|
|
588
|
-
const processBatch = async (batch, batchIdx) => {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
671
|
+
const processBatch = async (batch, batchIdx) => {
|
|
672
|
+
const batchData = {};
|
|
673
|
+
batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
|
|
674
|
+
const trayFile = `${packId_}_batch${batchIdx}.webp`;
|
|
675
|
+
batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
|
|
593
676
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
|
|
599
|
-
const uploadRes = await options.upload(upload.encFilePath, {
|
|
600
|
-
fileEncSha256B64: upload.fileEncSha256.toString('base64'),
|
|
601
|
-
mediaType: 'sticker-pack',
|
|
602
|
-
timeoutMs: options.mediaUploadTimeoutMs
|
|
603
|
-
});
|
|
604
|
-
await fs.unlink(upload.encFilePath);
|
|
605
|
-
|
|
606
|
-
let thumbBuf;
|
|
607
|
-
if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
|
|
608
|
-
else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
|
|
609
|
-
|
|
610
|
-
let thumbUploadRes;
|
|
611
|
-
if (thumbBuf?.length) {
|
|
612
|
-
const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
|
|
613
|
-
thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
|
|
614
|
-
fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'),
|
|
615
|
-
mediaType: 'thumbnail-sticker-pack',
|
|
616
|
-
timeoutMs: options.mediaUploadTimeoutMs
|
|
677
|
+
const zipBuf = await new Promise((resolve, reject) => { zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))); });
|
|
678
|
+
const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
|
|
679
|
+
const uploadRes = await options.upload(upload.encFilePath, {
|
|
680
|
+
fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
617
681
|
});
|
|
618
|
-
await fs.unlink(
|
|
619
|
-
}
|
|
682
|
+
await fs.unlink(upload.encFilePath);
|
|
620
683
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
684
|
+
let thumbBuf;
|
|
685
|
+
if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
|
|
686
|
+
else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
|
|
687
|
+
|
|
688
|
+
let thumbUploadRes;
|
|
689
|
+
if (thumbBuf?.length) {
|
|
690
|
+
const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
|
|
691
|
+
thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
|
|
692
|
+
fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
|
|
693
|
+
});
|
|
694
|
+
await fs.unlink(thumbUpload.encFilePath);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
name: `${name} (${batchIdx + 1})`, publisher, packDescription: description, stickerPackId: `${packId_}_${batchIdx}`,
|
|
699
|
+
stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
|
|
700
|
+
stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
|
|
701
|
+
fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
|
|
702
|
+
directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
|
|
703
|
+
...(thumbUploadRes && { thumbnailDirectPath: thumbUploadRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined })
|
|
704
|
+
};
|
|
636
705
|
};
|
|
637
|
-
};
|
|
638
706
|
|
|
639
|
-
if (validStickers.length > 60) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
707
|
+
if (validStickers.length > 60) {
|
|
708
|
+
const batches = [];
|
|
709
|
+
for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
|
|
710
|
+
const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
|
|
711
|
+
return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
|
|
712
|
+
}
|
|
645
713
|
|
|
646
|
-
return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
|
|
714
|
+
return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
|
|
647
715
|
}
|
|
648
716
|
|
|
649
717
|
export const assertMediaContent = (content) => {
|
|
@@ -651,5 +719,4 @@ export const assertMediaContent = (content) => {
|
|
|
651
719
|
const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage;
|
|
652
720
|
if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content });
|
|
653
721
|
return mediaContent;
|
|
654
|
-
};
|
|
655
|
-
|
|
722
|
+
};
|