@skyzopedia/baileys-mod 3.0.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,776 +1,908 @@
1
- //=======================================================//
2
- import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from "./messages-media.js";
3
- import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from "../Defaults/index.js";
4
- import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from "../WABinary/index.js";
5
- import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from "./generics.js";
6
- import { WAMessageStatus, WAProto } from "../Types/index.js";
7
- import { proto } from "../../WAProto/index.js";
8
- import { sha256 } from "./crypto.js";
9
- import { randomBytes } from "crypto";
10
- import { promises as fs } from "fs";
11
- import { Boom } from "@hapi/boom";
12
- import {} from "stream";
13
- //=======================================================//
1
+ import { Boom } from '@hapi/boom';
2
+ import { randomBytes } from 'crypto';
3
+ import { promises as fs } from 'fs';
4
+ import {} from 'stream';
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, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
14
12
  const MIMETYPE_MAP = {
15
- "image": "image/jpeg",
16
- "video": "video/mp4",
17
- "document": "application/pdf",
18
- "audio": "audio/ogg; codecs=opus",
19
- "sticker": "image/webp",
20
- "product-catalog-image": "image/jpeg"
13
+ image: 'image/jpeg',
14
+ video: 'video/mp4',
15
+ document: 'application/pdf',
16
+ audio: 'audio/ogg; codecs=opus',
17
+ sticker: 'image/webp',
18
+ 'product-catalog-image': 'image/jpeg'
21
19
  };
22
- //=======================================================//
23
20
  const MessageTypeProto = {
24
- "image": WAProto.Message.ImageMessage,
25
- "video": WAProto.Message.VideoMessage,
26
- "audio": WAProto.Message.AudioMessage,
27
- "sticker": WAProto.Message.StickerMessage,
28
- "document": WAProto.Message.DocumentMessage
21
+ image: WAProto.Message.ImageMessage,
22
+ video: WAProto.Message.VideoMessage,
23
+ audio: WAProto.Message.AudioMessage,
24
+ sticker: WAProto.Message.StickerMessage,
25
+ document: WAProto.Message.DocumentMessage
29
26
  };
30
- //=======================================================//
27
+ const ButtonType = proto.Message.ButtonsMessage.HeaderType;
28
+ /**
29
+ * Uses a regex to test whether the string contains a URL, and returns the URL if it does.
30
+ * @param text eg. hello https://google.com
31
+ * @returns the URL, eg. https://google.com
32
+ */
31
33
  export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
32
34
  export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
33
- const url = extractUrlFromText(text);
34
- if (!!getUrlInfo && url) {
35
- try {
36
- const urlInfo = await getUrlInfo(url);
37
- return urlInfo;
38
- }
39
- catch (error) {
40
- logger?.warn({ trace: error.stack }, "url generation failed");
35
+ const url = extractUrlFromText(text);
36
+ if (!!getUrlInfo && url) {
37
+ try {
38
+ const urlInfo = await getUrlInfo(url);
39
+ return urlInfo;
40
+ }
41
+ catch (error) {
42
+ // ignore if fails
43
+ logger?.warn({ trace: error.stack }, 'url generation failed');
44
+ }
41
45
  }
42
- }
43
46
  };
44
47
  const assertColor = async (color) => {
45
- let assertedColor;
46
- if (typeof color === "number") {
47
- assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1;
48
- }
49
- else {
50
- let hex = color.trim().replace("#", "");
51
- if (hex.length <= 6) {
52
- hex = "FF" + hex.padStart(6, "0");
53
- }
54
- assertedColor = parseInt(hex, 16);
55
- return assertedColor;
56
- }
48
+ let assertedColor;
49
+ if (typeof color === 'number') {
50
+ assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1;
51
+ }
52
+ else {
53
+ let hex = color.trim().replace('#', '');
54
+ if (hex.length <= 6) {
55
+ hex = 'FF' + hex.padStart(6, '0');
56
+ }
57
+ assertedColor = parseInt(hex, 16);
58
+ return assertedColor;
59
+ }
57
60
  };
58
- //=======================================================//
59
61
  export const prepareWAMessageMedia = async (message, options) => {
60
- const logger = options.logger;
61
- let mediaType;
62
- for (const key of MEDIA_KEYS) {
63
- if (key in message) {
64
- mediaType = key;
65
- }
66
- }
67
- if (!mediaType) {
68
- throw new Boom("Invalid media type", { statusCode: 400 });
69
- }
70
- const uploadData = {
71
- ...message,
72
- media: message[mediaType]
73
- };
74
- delete uploadData[mediaType];
75
- const cacheableKey = typeof uploadData.media === "object" &&
76
- "url" in uploadData.media &&
77
- !!uploadData.media.url &&
78
- !!options.mediaCache &&
79
- mediaType + ":" + uploadData.media.url.toString();
80
- if (mediaType === "document" && !uploadData.fileName) {
81
- uploadData.fileName = "file";
82
- }
83
- if (!uploadData.mimetype) {
84
- uploadData.mimetype = MIMETYPE_MAP[mediaType];
85
- }
86
- if (cacheableKey) {
87
- const mediaBuff = await options.mediaCache.get(cacheableKey);
88
- if (mediaBuff) {
89
- logger?.debug({ cacheableKey }, "got media cache hit");
90
- const obj = proto.Message.decode(mediaBuff);
91
- const key = `${mediaType}Message`;
92
- Object.assign(obj[key], { ...uploadData, media: undefined });
93
- return obj;
94
- }
95
- }
96
- const isNewsletter = !!options.jid && isJidNewsletter(options.jid);
97
- if (isNewsletter) {
98
- logger?.info({ key: cacheableKey }, "Preparing raw media for newsletter");
99
- const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(uploadData.media, options.mediaTypeOverride || mediaType, logger);
100
- const fileSha256B64 = fileSha256.toString("base64");
101
- const { mediaUrl, directPath } = await options.upload(filePath, {
102
- fileEncSha256B64: fileSha256B64,
103
- mediaType: mediaType,
104
- timeoutMs: options.mediaUploadTimeoutMs
62
+ const logger = options.logger;
63
+ let mediaType;
64
+ for (const key of MEDIA_KEYS) {
65
+ if (key in message) {
66
+ mediaType = key;
67
+ }
68
+ }
69
+ if (!mediaType) {
70
+ throw new Boom('Invalid media type', { statusCode: 400 });
71
+ }
72
+ const uploadData = {
73
+ ...message,
74
+ ...(message.annotations ? {
75
+ annotations: message.annotations
76
+ } : {
77
+ annotations: [
78
+ {
79
+ polygonVertices: [
80
+ {
81
+ x: 60.71664810180664,
82
+ y: -36.39784622192383
83
+ },
84
+ {
85
+ x: -16.710189819335938,
86
+ y: 49.263675689697266
87
+ },
88
+ {
89
+ x: -56.585853576660156,
90
+ y: 37.85963439941406
91
+ },
92
+ {
93
+ x: 20.840980529785156,
94
+ y: -47.80188751220703
95
+ }
96
+ ],
97
+ newsletter: {
98
+ newsletterJid: "120363420249672073@newsletter",
99
+ serverMessageId: 0,
100
+ newsletterName: "kyuu ilysm",
101
+ contentType: "UPDATE",
102
+ }
103
+ }
104
+ ]
105
+ }),
106
+ media: message[mediaType]
107
+ };
108
+ delete uploadData[mediaType];
109
+ // check if cacheable + generate cache key
110
+ const cacheableKey = typeof uploadData.media === 'object' &&
111
+ 'url' in uploadData.media &&
112
+ !!uploadData.media.url &&
113
+ !!options.mediaCache &&
114
+ mediaType + ':' + uploadData.media.url.toString();
115
+ if (mediaType === 'document' && !uploadData.fileName) {
116
+ uploadData.fileName = 'file';
117
+ }
118
+ if (!uploadData.mimetype) {
119
+ uploadData.mimetype = MIMETYPE_MAP[mediaType];
120
+ }
121
+ if (cacheableKey) {
122
+ const mediaBuff = await options.mediaCache.get(cacheableKey);
123
+ if (mediaBuff) {
124
+ logger?.debug({ cacheableKey }, 'got media cache hit');
125
+ const obj = proto.Message.decode(mediaBuff);
126
+ const key = `${mediaType}Message`;
127
+ Object.assign(obj[key], { ...uploadData, media: undefined });
128
+ return obj;
129
+ }
130
+ }
131
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid);
132
+ if (isNewsletter) {
133
+ logger?.info({ key: cacheableKey }, 'Preparing raw media for newsletter');
134
+ const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(uploadData.media, options.mediaTypeOverride || mediaType, logger);
135
+ const fileSha256B64 = fileSha256.toString('base64');
136
+ const { mediaUrl, directPath } = await options.upload(filePath, {
137
+ fileEncSha256B64: fileSha256B64,
138
+ mediaType: mediaType,
139
+ timeoutMs: options.mediaUploadTimeoutMs
140
+ });
141
+ await fs.unlink(filePath);
142
+ const obj = WAProto.Message.fromObject({
143
+ // todo: add more support here
144
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
145
+ url: mediaUrl,
146
+ directPath,
147
+ fileSha256,
148
+ fileLength,
149
+ ...uploadData,
150
+ media: undefined
151
+ })
152
+ });
153
+ if (uploadData.ptv) {
154
+ obj.ptvMessage = obj.videoMessage;
155
+ delete obj.videoMessage;
156
+ }
157
+ if (obj.stickerMessage) {
158
+ obj.stickerMessage.stickerSentTs = Date.now();
159
+ }
160
+ if (cacheableKey) {
161
+ logger?.debug({ cacheableKey }, 'set cache');
162
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
163
+ }
164
+ return obj;
165
+ }
166
+ const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
167
+ const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
168
+ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
169
+ const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
170
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
171
+ const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
172
+ logger,
173
+ saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
174
+ opts: options.options
175
+ });
176
+ const fileEncSha256B64 = fileEncSha256.toString('base64');
177
+ const [{ mediaUrl, directPath }] = await Promise.all([
178
+ (async () => {
179
+ const result = await options.upload(encFilePath, {
180
+ fileEncSha256B64,
181
+ mediaType,
182
+ timeoutMs: options.mediaUploadTimeoutMs
183
+ });
184
+ logger?.debug({ mediaType, cacheableKey }, 'uploaded media');
185
+ return result;
186
+ })(),
187
+ (async () => {
188
+ try {
189
+ if (requiresThumbnailComputation) {
190
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
191
+ uploadData.jpegThumbnail = thumbnail;
192
+ if (!uploadData.width && originalImageDimensions) {
193
+ uploadData.width = originalImageDimensions.width;
194
+ uploadData.height = originalImageDimensions.height;
195
+ logger?.debug('set dimensions');
196
+ }
197
+ logger?.debug('generated thumbnail');
198
+ }
199
+ if (requiresDurationComputation) {
200
+ uploadData.seconds = await getAudioDuration(originalFilePath);
201
+ logger?.debug('computed audio duration');
202
+ }
203
+ if (requiresWaveformProcessing) {
204
+ uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
205
+ logger?.debug('processed waveform');
206
+ }
207
+ if (requiresAudioBackground) {
208
+ uploadData.backgroundArgb = await assertColor(options.backgroundColor);
209
+ logger?.debug('computed backgroundColor audio status');
210
+ }
211
+ }
212
+ catch (error) {
213
+ logger?.warn({ trace: error.stack }, 'failed to obtain extra info');
214
+ }
215
+ })()
216
+ ]).finally(async () => {
217
+ try {
218
+ await fs.unlink(encFilePath);
219
+ if (originalFilePath) {
220
+ await fs.unlink(originalFilePath);
221
+ }
222
+ logger?.debug('removed tmp files');
223
+ }
224
+ catch (error) {
225
+ logger?.warn('failed to remove tmp file');
226
+ }
105
227
  });
106
- await fs.unlink(filePath);
107
228
  const obj = WAProto.Message.fromObject({
108
- [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
109
- url: mediaUrl,
110
- directPath,
111
- fileSha256,
112
- fileLength,
113
- ...uploadData,
114
- media: undefined
115
- })
229
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
230
+ url: mediaUrl,
231
+ directPath,
232
+ mediaKey,
233
+ fileEncSha256,
234
+ fileSha256,
235
+ fileLength,
236
+ mediaKeyTimestamp: unixTimestampSeconds(),
237
+ ...uploadData,
238
+ media: undefined
239
+ })
116
240
  });
117
241
  if (uploadData.ptv) {
118
- obj.ptvMessage = obj.videoMessage;
119
- delete obj.videoMessage;
120
- }
121
- if (obj.stickerMessage) {
122
- obj.stickerMessage.stickerSentTs = Date.now();
242
+ obj.ptvMessage = obj.videoMessage;
243
+ delete obj.videoMessage;
123
244
  }
124
245
  if (cacheableKey) {
125
- logger?.debug({ cacheableKey }, "set cache");
126
- await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
246
+ logger?.debug({ cacheableKey }, 'set cache');
247
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
127
248
  }
128
249
  return obj;
129
- }
130
- const requiresDurationComputation = mediaType === "audio" && typeof uploadData.seconds === "undefined";
131
- const requiresThumbnailComputation = (mediaType === "image" || mediaType === "video") && typeof uploadData["jpegThumbnail"] === "undefined";
132
- const requiresWaveformProcessing = mediaType === "audio" && uploadData.ptt === true;
133
- const requiresAudioBackground = options.backgroundColor && mediaType === "audio" && uploadData.ptt === true;
134
- const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
135
- const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
136
- logger,
137
- saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
138
- opts: options.options
139
- });
140
- const fileEncSha256B64 = fileEncSha256.toString("base64");
141
- const [{ mediaUrl, directPath }] = await Promise.all([
142
- (async () => {
143
- const result = await options.upload(encFilePath, {
144
- fileEncSha256B64,
145
- mediaType,
146
- timeoutMs: options.mediaUploadTimeoutMs
147
- });
148
- logger?.debug({ mediaType, cacheableKey }, "uploaded media");
149
- return result;
150
- })(),
151
- (async () => {
152
- try {
153
- if (requiresThumbnailComputation) {
154
- const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
155
- uploadData.jpegThumbnail = thumbnail;
156
- if (!uploadData.width && originalImageDimensions) {
157
- uploadData.width = originalImageDimensions.width;
158
- uploadData.height = originalImageDimensions.height;
159
- logger?.debug("set dimensions");
160
- }
161
- logger?.debug("generated thumbnail");
162
- }
163
- if (requiresDurationComputation) {
164
- uploadData.seconds = await getAudioDuration(originalFilePath);
165
- logger?.debug("computed audio duration");
166
- }
167
- if (requiresWaveformProcessing) {
168
- uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
169
- logger?.debug("processed waveform");
170
- }
171
- if (requiresAudioBackground) {
172
- uploadData.backgroundArgb = await assertColor(options.backgroundColor);
173
- logger?.debug("computed backgroundColor audio status");
174
- }
175
- }
176
- catch (error) {
177
- logger?.warn({ trace: error.stack }, "failed to obtain extra info");
178
- }
179
- })()
180
- ]).finally(async () => {
181
- try {
182
- await fs.unlink(encFilePath);
183
- if (originalFilePath) {
184
- await fs.unlink(originalFilePath);
185
- }
186
- logger?.debug("removed tmp files");
187
- }
188
- catch (error) {
189
- logger?.warn("failed to remove tmp file");
190
- }
191
- });
192
- const obj = WAProto.Message.fromObject({
193
- [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
194
- url: mediaUrl,
195
- directPath,
196
- mediaKey,
197
- fileEncSha256,
198
- fileSha256,
199
- fileLength,
200
- mediaKeyTimestamp: unixTimestampSeconds(),
201
- ...uploadData,
202
- media: undefined
203
- })
204
- });
205
- if (uploadData.ptv) {
206
- obj.ptvMessage = obj.videoMessage;
207
- delete obj.videoMessage;
208
- }
209
- if (cacheableKey) {
210
- logger?.debug({ cacheableKey }, "set cache");
211
- await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
212
- }
213
- return obj;
214
250
  };
215
- //=======================================================//
216
251
  export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
217
- ephemeralExpiration = ephemeralExpiration || 0;
218
- const content = {
219
- ephemeralMessage: {
220
- message: {
221
- protocolMessage: {
222
- type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
223
- ephemeralExpiration
224
- }
225
- }
226
- }
227
- };
228
- return WAProto.Message.fromObject(content);
252
+ ephemeralExpiration = ephemeralExpiration || 0;
253
+ const content = {
254
+ ephemeralMessage: {
255
+ message: {
256
+ protocolMessage: {
257
+ type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
258
+ ephemeralExpiration
259
+ }
260
+ }
261
+ }
262
+ };
263
+ return WAProto.Message.fromObject(content);
229
264
  };
230
- //=======================================================//
265
+ /**
266
+ * Generate forwarded message content like WA does
267
+ * @param message the message to forward
268
+ * @param options.forceForward will show the message as forwarded even if it is from you
269
+ */
231
270
  export const generateForwardMessageContent = (message, forceForward) => {
232
- let content = message.message;
233
- if (!content) {
234
- throw new Boom("no content in message", { statusCode: 400 });
235
- }
236
- content = normalizeMessageContent(content);
237
- content = proto.Message.decode(proto.Message.encode(content).finish());
238
- let key = Object.keys(content)[0];
239
- let score = content?.[key]?.contextInfo?.forwardingScore || 0;
240
- score += message.key.fromMe && !forceForward ? 0 : 1;
241
- if (key === "conversation") {
242
- content.extendedTextMessage = { text: content[key] };
243
- delete content.conversation;
244
- key = "extendedTextMessage";
245
- }
246
- const key_ = content?.[key];
247
- if (score > 0) {
248
- key_.contextInfo = { forwardingScore: score, isForwarded: true };
249
- }
250
- else {
251
- key_.contextInfo = {};
252
- }
253
- return content;
271
+ let content = message.message;
272
+ if (!content) {
273
+ throw new Boom('no content in message', { statusCode: 400 });
274
+ }
275
+ // hacky copy
276
+ content = normalizeMessageContent(content);
277
+ content = proto.Message.decode(proto.Message.encode(content).finish());
278
+ let key = Object.keys(content)[0];
279
+ let score = content?.[key]?.contextInfo?.forwardingScore || 0;
280
+ score += message.key.fromMe && !forceForward ? 0 : 1;
281
+ if (key === 'conversation') {
282
+ content.extendedTextMessage = { text: content[key] };
283
+ delete content.conversation;
284
+ key = 'extendedTextMessage';
285
+ }
286
+ const key_ = content?.[key];
287
+ if (score > 0) {
288
+ key_.contextInfo = { forwardingScore: score, isForwarded: true };
289
+ }
290
+ else {
291
+ key_.contextInfo = {};
292
+ }
293
+ return content;
254
294
  };
255
295
  export const generateWAMessageContent = async (message, options) => {
256
- var _a, _b;
257
- let m = {};
258
- if ("text" in message) {
259
- const extContent = { text: message.text };
260
- let urlInfo = message.linkPreview;
261
- if (typeof urlInfo === "undefined") {
262
- urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
263
- }
264
- if (urlInfo) {
265
- extContent.matchedText = urlInfo["matched-text"];
266
- extContent.jpegThumbnail = urlInfo.jpegThumbnail;
267
- extContent.description = urlInfo.description;
268
- extContent.title = urlInfo.title;
269
- extContent.previewType = 0;
270
- const img = urlInfo.highQualityThumbnail;
271
- if (img) {
272
- extContent.thumbnailDirectPath = img.directPath;
273
- extContent.mediaKey = img.mediaKey;
274
- extContent.mediaKeyTimestamp = img.mediaKeyTimestamp;
275
- extContent.thumbnailWidth = img.width;
276
- extContent.thumbnailHeight = img.height;
277
- extContent.thumbnailSha256 = img.fileSha256;
278
- extContent.thumbnailEncSha256 = img.fileEncSha256;
279
- }
280
- }
281
- if (options.backgroundColor) {
282
- extContent.backgroundArgb = await assertColor(options.backgroundColor);
283
- }
284
- if (options.font) {
285
- extContent.font = options.font;
286
- }
287
- m.extendedTextMessage = extContent;
288
- }
289
- else if ("contacts" in message) {
290
- const contactLen = message.contacts.contacts.length;
291
- if (!contactLen) {
292
- throw new Boom("require atleast 1 contact", { statusCode: 400 });
293
- }
294
- if (contactLen === 1) {
295
- m.contactMessage = WAProto.Message.ContactMessage.fromObject(message.contacts.contacts[0]);
296
+ var _a, _b;
297
+ let m = {};
298
+ if ('text' in message) {
299
+ const extContent = { text: message.text };
300
+ let urlInfo = message.linkPreview;
301
+ if (typeof urlInfo === 'undefined') {
302
+ urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
303
+ }
304
+ if (urlInfo) {
305
+ extContent.matchedText = urlInfo['matched-text'];
306
+ extContent.jpegThumbnail = urlInfo.jpegThumbnail;
307
+ extContent.description = urlInfo.description;
308
+ extContent.title = urlInfo.title;
309
+ extContent.previewType = 0;
310
+ const img = urlInfo.highQualityThumbnail;
311
+ if (img) {
312
+ extContent.thumbnailDirectPath = img.directPath;
313
+ extContent.mediaKey = img.mediaKey;
314
+ extContent.mediaKeyTimestamp = img.mediaKeyTimestamp;
315
+ extContent.thumbnailWidth = img.width;
316
+ extContent.thumbnailHeight = img.height;
317
+ extContent.thumbnailSha256 = img.fileSha256;
318
+ extContent.thumbnailEncSha256 = img.fileEncSha256;
319
+ }
320
+ }
321
+ if (options.backgroundColor) {
322
+ extContent.backgroundArgb = await assertColor(options.backgroundColor);
323
+ }
324
+ if (options.font) {
325
+ extContent.font = options.font;
326
+ }
327
+ m.extendedTextMessage = extContent;
296
328
  }
297
- else {
298
- m.contactsArrayMessage = WAProto.Message.ContactsArrayMessage.fromObject(message.contacts);
299
- }
300
- }
301
- else if ("location" in message) {
302
- m.locationMessage = WAProto.Message.LocationMessage.fromObject(message.location);
303
- }
304
- else if ("react" in message) {
305
- if (!message.react.senderTimestampMs) {
306
- message.react.senderTimestampMs = Date.now();
307
- }
308
- m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(message.react);
309
- }
310
- else if ("delete" in message) {
311
- m.protocolMessage = {
312
- key: message.delete,
313
- type: WAProto.Message.ProtocolMessage.Type.REVOKE
314
- };
315
- }
316
- else if ("forward" in message) {
317
- m = generateForwardMessageContent(message.forward, message.force);
318
- }
319
- else if ("disappearingMessagesInChat" in message) {
320
- const exp = typeof message.disappearingMessagesInChat === "boolean"
321
- ? message.disappearingMessagesInChat
322
- ? WA_DEFAULT_EPHEMERAL
323
- : 0
324
- : message.disappearingMessagesInChat;
325
- m = prepareDisappearingMessageSettingContent(exp);
326
- }
327
- else if ("groupInvite" in message) {
328
- m.groupInviteMessage = {};
329
- m.groupInviteMessage.inviteCode = message.groupInvite.inviteCode;
330
- m.groupInviteMessage.inviteExpiration = message.groupInvite.inviteExpiration;
331
- m.groupInviteMessage.caption = message.groupInvite.text;
332
- m.groupInviteMessage.groupJid = message.groupInvite.jid;
333
- m.groupInviteMessage.groupName = message.groupInvite.subject;
334
- if (options.getProfilePicUrl) {
335
- const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, "preview");
336
- if (pfpUrl) {
337
- const resp = await fetch(pfpUrl, { method: "GET", dispatcher: options?.options?.dispatcher });
338
- if (resp.ok) {
339
- const buf = Buffer.from(await resp.arrayBuffer());
340
- m.groupInviteMessage.jpegThumbnail = buf;
341
- }
342
- }
343
- }
344
- }
345
- else if ("pin" in message) {
346
- m.pinInChatMessage = {};
347
- m.messageContextInfo = {};
348
- m.pinInChatMessage.key = message.pin;
349
- m.pinInChatMessage.type = message.type;
350
- m.pinInChatMessage.senderTimestampMs = Date.now();
351
- m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
352
- }
353
- else if ("buttonReply" in message) {
354
- switch (message.type) {
355
- case "template":
356
- m.templateButtonReplyMessage = {
357
- selectedDisplayText: message.buttonReply.displayText,
358
- selectedId: message.buttonReply.id,
359
- selectedIndex: message.buttonReply.index
329
+ else if ('contacts' in message) {
330
+ const contactLen = message.contacts.contacts.length;
331
+ if (!contactLen) {
332
+ throw new Boom('require atleast 1 contact', { statusCode: 400 });
333
+ }
334
+ if (contactLen === 1) {
335
+ m.contactMessage = WAProto.Message.ContactMessage.create(message.contacts.contacts[0]);
336
+ }
337
+ else {
338
+ m.contactsArrayMessage = WAProto.Message.ContactsArrayMessage.create(message.contacts);
339
+ }
340
+ }
341
+ else if ('location' in message) {
342
+ m.locationMessage = WAProto.Message.LocationMessage.create(message.location);
343
+ }
344
+ else if ('react' in message) {
345
+ if (!message.react.senderTimestampMs) {
346
+ message.react.senderTimestampMs = Date.now();
347
+ }
348
+ m.reactionMessage = WAProto.Message.ReactionMessage.create(message.react);
349
+ }
350
+ else if ('delete' in message) {
351
+ m.protocolMessage = {
352
+ key: message.delete,
353
+ type: WAProto.Message.ProtocolMessage.Type.REVOKE
354
+ };
355
+ }
356
+ else if ('forward' in message) {
357
+ m = generateForwardMessageContent(message.forward, message.force);
358
+ }
359
+ else if ('disappearingMessagesInChat' in message) {
360
+ const exp = typeof message.disappearingMessagesInChat === 'boolean'
361
+ ? message.disappearingMessagesInChat
362
+ ? WA_DEFAULT_EPHEMERAL
363
+ : 0
364
+ : message.disappearingMessagesInChat;
365
+ m = prepareDisappearingMessageSettingContent(exp);
366
+ }
367
+ else if ('groupInvite' in message) {
368
+ m.groupInviteMessage = {};
369
+ m.groupInviteMessage.inviteCode = message.groupInvite.inviteCode;
370
+ m.groupInviteMessage.inviteExpiration = message.groupInvite.inviteExpiration;
371
+ m.groupInviteMessage.caption = message.groupInvite.text;
372
+ m.groupInviteMessage.groupJid = message.groupInvite.jid;
373
+ m.groupInviteMessage.groupName = message.groupInvite.subject;
374
+ //TODO: use built-in interface and get disappearing mode info etc.
375
+ //TODO: cache / use store!?
376
+ if (options.getProfilePicUrl) {
377
+ const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
378
+ if (pfpUrl) {
379
+ const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
380
+ if (resp.ok) {
381
+ const buf = Buffer.from(await resp.arrayBuffer());
382
+ m.groupInviteMessage.jpegThumbnail = buf;
383
+ }
384
+ }
385
+ }
386
+ }
387
+ else if ('pin' in message) {
388
+ m.pinInChatMessage = {};
389
+ m.messageContextInfo = {};
390
+ m.pinInChatMessage.key = message.pin;
391
+ m.pinInChatMessage.type = message.type;
392
+ m.pinInChatMessage.senderTimestampMs = Date.now();
393
+ m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
394
+ }
395
+ else if ('buttonReply' in message) {
396
+ switch (message.type) {
397
+ case 'template':
398
+ m.templateButtonReplyMessage = {
399
+ selectedDisplayText: message.buttonReply.displayText,
400
+ selectedId: message.buttonReply.id,
401
+ selectedIndex: message.buttonReply.index
402
+ };
403
+ break;
404
+ case 'plain':
405
+ m.buttonsResponseMessage = {
406
+ selectedButtonId: message.buttonReply.id,
407
+ selectedDisplayText: message.buttonReply.displayText,
408
+ type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
409
+ };
410
+ break;
411
+ }
412
+ }
413
+ else if ('ptv' in message && message.ptv) {
414
+ const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
415
+ m.ptvMessage = videoMessage;
416
+ }
417
+ else if ('product' in message) {
418
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
419
+ m.productMessage = WAProto.Message.ProductMessage.create({
420
+ ...message,
421
+ product: {
422
+ ...message.product,
423
+ productImage: imageMessage
424
+ }
425
+ });
426
+ }
427
+ else if ('listReply' in message) {
428
+ m.listResponseMessage = { ...message.listReply };
429
+ }
430
+ else if ('event' in message) {
431
+ m.eventMessage = {};
432
+ const startTime = Math.floor(message.event.startDate.getTime() / 1000);
433
+ if (message.event.call && options.getCallLink) {
434
+ const token = await options.getCallLink(message.event.call, { startTime });
435
+ m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
436
+ }
437
+ m.messageContextInfo = {
438
+ // encKey
439
+ messageSecret: message.event.messageSecret || randomBytes(32)
360
440
  };
361
- break;
362
- case "plain":
363
- m.buttonsResponseMessage = {
364
- selectedButtonId: message.buttonReply.id,
365
- selectedDisplayText: message.buttonReply.displayText,
366
- type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
441
+ m.eventMessage.name = message.event.name;
442
+ m.eventMessage.description = message.event.description;
443
+ m.eventMessage.startTime = startTime;
444
+ m.eventMessage.endTime = message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined;
445
+ m.eventMessage.isCanceled = message.event.isCancelled ?? false;
446
+ m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
447
+ m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
448
+ m.eventMessage.location = message.event.location;
449
+ }
450
+ else if ('poll' in message) {
451
+ (_a = message.poll).selectableCount || (_a.selectableCount = 0);
452
+ (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
453
+ if (!Array.isArray(message.poll.values)) {
454
+ throw new Boom('Invalid poll values', { statusCode: 400 });
455
+ }
456
+ if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length) {
457
+ throw new Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, {
458
+ statusCode: 400
459
+ });
460
+ }
461
+ m.messageContextInfo = {
462
+ // encKey
463
+ messageSecret: message.poll.messageSecret || randomBytes(32)
464
+ };
465
+ const pollCreationMessage = {
466
+ name: message.poll.name,
467
+ selectableOptionsCount: message.poll.selectableCount,
468
+ options: message.poll.values.map(optionName => ({ optionName }))
469
+ };
470
+ if (message.poll.toAnnouncementGroup) {
471
+ // poll v2 is for community announcement groups (single select and multiple)
472
+ m.pollCreationMessageV2 = pollCreationMessage;
473
+ }
474
+ else {
475
+ if (message.poll.selectableCount === 1) {
476
+ //poll v3 is for single select polls
477
+ m.pollCreationMessageV3 = pollCreationMessage;
478
+ }
479
+ else {
480
+ // poll for multiple choice polls
481
+ m.pollCreationMessage = pollCreationMessage;
482
+ }
483
+ }
484
+ }
485
+ else if ('sharePhoneNumber' in message) {
486
+ m.protocolMessage = {
487
+ type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
488
+ };
489
+ }
490
+ else if ('requestPhoneNumber' in message) {
491
+ m.requestPhoneNumberMessage = {};
492
+ }
493
+ else if ('limitSharing' in message) {
494
+ m.protocolMessage = {
495
+ type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
496
+ limitSharing: {
497
+ sharingLimited: message.limitSharing === true,
498
+ trigger: 1,
499
+ limitSharingSettingTimestamp: Date.now(),
500
+ initiatedByMe: true
501
+ }
367
502
  };
368
- break;
369
- }
370
- }
371
- else if ("ptv" in message && message.ptv) {
372
- const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
373
- m.ptvMessage = videoMessage;
374
- }
375
- else if ("product" in message) {
376
- const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
377
- m.productMessage = WAProto.Message.ProductMessage.fromObject({
378
- ...message,
379
- product: {
380
- ...message.product,
381
- productImage: imageMessage
382
- }
383
- });
384
- }
385
- else if ("listReply" in message) {
386
- m.listResponseMessage = { ...message.listReply };
387
- }
388
- else if ("event" in message) {
389
- m.eventMessage = {};
390
- const startTime = Math.floor(message.event.startDate.getTime() / 1000);
391
- if (message.event.call && options.getCallLink) {
392
- const token = await options.getCallLink(message.event.call, { startTime });
393
- m.eventMessage.joinLink = (message.event.call === "audio" ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
394
- }
395
- m.messageContextInfo = {
396
- messageSecret: message.event.messageSecret || randomBytes(32)
397
- };
398
- m.eventMessage.name = message.event.name;
399
- m.eventMessage.description = message.event.description;
400
- m.eventMessage.startTime = startTime;
401
- m.eventMessage.endTime = message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined;
402
- m.eventMessage.isCanceled = message.event.isCancelled ?? false;
403
- m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
404
- m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
405
- m.eventMessage.location = message.event.location;
406
- }
407
- else if ("poll" in message) {
408
- (_a = message.poll).selectableCount || (_a.selectableCount = 0);
409
- (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
410
- if (!Array.isArray(message.poll.values)) {
411
- throw new Boom("Invalid poll values", { statusCode: 400 });
412
- }
413
- if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length) {
414
- throw new Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, {
415
- statusCode: 400
416
- });
417
- }
418
- m.messageContextInfo = {
419
- messageSecret: message.poll.messageSecret || randomBytes(32)
420
- };
421
- const pollCreationMessage = {
422
- name: message.poll.name,
423
- selectableOptionsCount: message.poll.selectableCount,
424
- options: message.poll.values.map(optionName => ({ optionName }))
425
- };
426
- if (message.poll.toAnnouncementGroup) {
427
- m.pollCreationMessageV2 = pollCreationMessage;
428
503
  }
429
504
  else {
430
- if (message.poll.selectableCount === 1) {
431
- m.pollCreationMessageV3 = pollCreationMessage;
432
- }
433
- else {
434
- m.pollCreationMessage = pollCreationMessage;
435
- }
436
- }
437
- }
438
- else if ("sharePhoneNumber" in message) {
439
- m.protocolMessage = {
440
- type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
441
- };
442
- }
443
- else if ("requestPhoneNumber" in message) {
444
- m.requestPhoneNumberMessage = {};
445
- }
446
- else if ("limitSharing" in message) {
447
- m.protocolMessage = {
448
- type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
449
- limitSharing: {
450
- sharingLimited: message.limitSharing === true,
451
- trigger: 1,
452
- limitSharingSettingTimestamp: Date.now(),
453
- initiatedByMe: true
454
- }
455
- };
456
- }
457
- else {
458
- m = await prepareWAMessageMedia(message, options);
459
- }
460
- if ("viewOnce" in message && !!message.viewOnce) {
461
- m = { viewOnceMessage: { message: m } };
462
- }
463
- if ("mentions" in message && message.mentions?.length) {
464
- const messageType = Object.keys(m)[0];
465
- const key = m[messageType];
466
- if ("contextInfo" in key && !!key.contextInfo) {
467
- key.contextInfo.mentionedJid = message.mentions;
468
- }
469
- else if (key) {
470
- key.contextInfo = {
471
- mentionedJid: message.mentions
472
- };
473
- }
474
- }
475
- if ("edit" in message) {
476
- m = {
477
- protocolMessage: {
478
- key: message.edit,
479
- editedMessage: m,
480
- timestampMs: Date.now(),
481
- type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
482
- }
483
- };
484
- }
485
- if ("contextInfo" in message && !!message.contextInfo) {
486
- const messageType = Object.keys(m)[0];
487
- const key = m[messageType];
488
- if ("contextInfo" in key && !!key.contextInfo) {
489
- key.contextInfo = { ...key.contextInfo, ...message.contextInfo };
490
- }
491
- else if (key) {
492
- key.contextInfo = message.contextInfo;
493
- }
494
- }
495
- return WAProto.Message.fromObject(m);
505
+ m = await prepareWAMessageMedia(message, options);
506
+ }
507
+ if ('buttons' in message && !!message.buttons) {
508
+ const buttonsMessage = {
509
+ buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE }))
510
+ };
511
+ if ('text' in message) {
512
+ buttonsMessage.contentText = message.text;
513
+ buttonsMessage.headerType = ButtonType.EMPTY;
514
+ }
515
+ else {
516
+ if ('caption' in message) {
517
+ buttonsMessage.contentText = message.caption;
518
+ }
519
+ const type = Object.keys(m)[0].replace('Message', '').toUpperCase();
520
+ buttonsMessage.headerType = ButtonType[type];
521
+ Object.assign(buttonsMessage, m);
522
+ }
523
+ if ('footer' in message && !!message.footer) {
524
+ buttonsMessage.footerText = message.footer;
525
+ }
526
+ m = { buttonsMessage };
527
+ }
528
+ else if ('templateButtons' in message && !!message.templateButtons) {
529
+ const msg = {
530
+ hydratedButtons: message.templateButtons
531
+ };
532
+ if ('text' in message) {
533
+ msg.hydratedContentText = message.text;
534
+ }
535
+ else {
536
+ if ('caption' in message) {
537
+ msg.hydratedContentText = message.caption;
538
+ }
539
+ Object.assign(msg, m);
540
+ }
541
+ if ('footer' in message && !!message.footer) {
542
+ msg.hydratedFooterText = message.footer;
543
+ }
544
+ m = {
545
+ templateMessage: {
546
+ fourRowTemplate: msg,
547
+ hydratedTemplate: msg
548
+ }
549
+ };
550
+ }
551
+ if ('sections' in message && !!message.sections) {
552
+ const listMessage = {
553
+ sections: message.sections,
554
+ buttonText: message.buttonText,
555
+ title: message.title,
556
+ footerText: message.footer,
557
+ description: message.text,
558
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT
559
+ };
560
+ m = { listMessage };
561
+ }
562
+ if ('viewOnce' in message && !!message.viewOnce) {
563
+ m = { viewOnceMessage: { message: m } };
564
+ }
565
+ if ('mentions' in message && message.mentions?.length) {
566
+ const messageType = Object.keys(m)[0];
567
+ const key = m[messageType];
568
+ if ('contextInfo' in key && !!key.contextInfo) {
569
+ key.contextInfo.mentionedJid = message.mentions;
570
+ }
571
+ else if (key) {
572
+ key.contextInfo = {
573
+ mentionedJid: message.mentions
574
+ };
575
+ }
576
+ }
577
+ if ('edit' in message) {
578
+ m = {
579
+ protocolMessage: {
580
+ key: message.edit,
581
+ editedMessage: m,
582
+ timestampMs: Date.now(),
583
+ type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
584
+ }
585
+ };
586
+ }
587
+ if ('contextInfo' in message && !!message.contextInfo) {
588
+ const messageType = Object.keys(m)[0];
589
+ const key = m[messageType];
590
+ if ('contextInfo' in key && !!key.contextInfo) {
591
+ key.contextInfo = { ...key.contextInfo, ...message.contextInfo };
592
+ }
593
+ else if (key) {
594
+ key.contextInfo = message.contextInfo;
595
+ }
596
+ }
597
+ return WAProto.Message.create(m);
496
598
  };
497
- //=======================================================//
498
599
  export const generateWAMessageFromContent = (jid, message, options) => {
499
- if (!options.timestamp) {
500
- options.timestamp = new Date();
501
- }
502
- const innerMessage = normalizeMessageContent(message);
503
- const key = getContentType(innerMessage);
504
- const timestamp = unixTimestampSeconds(options.timestamp);
505
- const { quoted, userJid } = options;
506
- if (quoted && !isJidNewsletter(jid)) {
507
- const participant = quoted.key.fromMe
508
- ? userJid
509
- : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
510
- let quotedMsg = normalizeMessageContent(quoted.message);
511
- const msgType = getContentType(quotedMsg);
512
- quotedMsg = proto.Message.fromObject({ [msgType]: quotedMsg[msgType] });
513
- const quotedContent = quotedMsg[msgType];
514
- if (typeof quotedContent === "object" && quotedContent && "contextInfo" in quotedContent) {
515
- delete quotedContent.contextInfo;
516
- }
517
- const contextInfo = ("contextInfo" in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
518
- contextInfo.participant = jidNormalizedUser(participant);
519
- contextInfo.stanzaId = quoted.key.id;
520
- contextInfo.quotedMessage = quotedMsg;
521
- if (jid !== quoted.key.remoteJid) {
522
- contextInfo.remoteJid = quoted.key.remoteJid;
523
- }
524
- if (contextInfo && innerMessage[key]) {
525
- innerMessage[key].contextInfo = contextInfo;
526
- }
527
- }
528
- if (
529
- !!options?.ephemeralExpiration &&
530
- key !== "protocolMessage" &&
531
- key !== "ephemeralMessage" &&
532
- !isJidNewsletter(jid)) {
533
- innerMessage[key].contextInfo = {
534
- ...(innerMessage[key].contextInfo || {}),
535
- expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL
600
+ // set timestamp to now
601
+ // if not specified
602
+ if (!options.timestamp) {
603
+ options.timestamp = new Date();
604
+ }
605
+ const innerMessage = normalizeMessageContent(message);
606
+ const key = getContentType(innerMessage);
607
+ const timestamp = unixTimestampSeconds(options.timestamp);
608
+ const { quoted, userJid } = options;
609
+ if (quoted && !isJidNewsletter(jid)) {
610
+ const participant = quoted.key.fromMe
611
+ ? userJid // TODO: Add support for LIDs
612
+ : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
613
+ let quotedMsg = normalizeMessageContent(quoted.message);
614
+ const msgType = getContentType(quotedMsg);
615
+ // strip any redundant properties
616
+ quotedMsg = proto.Message.create({ [msgType]: quotedMsg[msgType] });
617
+ const quotedContent = quotedMsg[msgType];
618
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
619
+ delete quotedContent.contextInfo;
620
+ }
621
+ const contextInfo = ('contextInfo' in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
622
+ contextInfo.participant = jidNormalizedUser(participant);
623
+ contextInfo.stanzaId = quoted.key.id;
624
+ contextInfo.quotedMessage = quotedMsg;
625
+ // if a participant is quoted, then it must be a group
626
+ // hence, remoteJid of group must also be entered
627
+ if (jid !== quoted.key.remoteJid) {
628
+ contextInfo.remoteJid = quoted.key.remoteJid;
629
+ }
630
+ if (contextInfo && innerMessage[key]) {
631
+ /* @ts-ignore */
632
+ innerMessage[key].contextInfo = contextInfo;
633
+ }
634
+ }
635
+ if (
636
+ // if we want to send a disappearing message
637
+ !!options?.ephemeralExpiration &&
638
+ // and it's not a protocol message -- delete, toggle disappear message
639
+ key !== 'protocolMessage' &&
640
+ // already not converted to disappearing message
641
+ key !== 'ephemeralMessage' &&
642
+ // newsletters don't support ephemeral messages
643
+ !isJidNewsletter(jid)) {
644
+ /* @ts-ignore */
645
+ innerMessage[key].contextInfo = {
646
+ ...(innerMessage[key].contextInfo || {}),
647
+ expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL
648
+ //ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
649
+ };
650
+ }
651
+ message = WAProto.Message.create(message);
652
+ const messageJSON = {
653
+ key: {
654
+ remoteJid: jid,
655
+ fromMe: true,
656
+ id: options?.messageId || generateMessageIDV2()
657
+ },
658
+ message: message,
659
+ messageTimestamp: timestamp,
660
+ messageStubParameters: [],
661
+ participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined, // TODO: Add support for LIDs
662
+ status: WAMessageStatus.PENDING
536
663
  };
537
- }
538
- message = WAProto.Message.fromObject(message);
539
- const messageJSON = {
540
- key: {
541
- remoteJid: jid,
542
- fromMe: true,
543
- id: options?.messageId || generateMessageIDV2()
544
- },
545
- message: message,
546
- messageTimestamp: timestamp,
547
- messageStubParameters: [],
548
- participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
549
- status: WAMessageStatus.PENDING
550
- };
551
- return WAProto.WebMessageInfo.fromObject(messageJSON);
664
+ return WAProto.WebMessageInfo.fromObject(messageJSON);
552
665
  };
553
- //=======================================================//
554
666
  export const generateWAMessage = async (jid, content, options) => {
555
- options.logger = options?.logger?.child({ msgId: options.messageId });
556
- return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
667
+ // ensure msg ID is with every log
668
+ options.logger = options?.logger?.child({ msgId: options.messageId });
669
+ // Pass jid in the options to generateWAMessageContent
670
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
557
671
  };
558
- //=======================================================//
672
+ /** Get the key to access the true type of content */
559
673
  export const getContentType = (content) => {
560
- if (content) {
561
- const keys = Object.keys(content);
562
- const key = keys.find(k => (k === "conversation" || k.includes("Message")) && k !== "senderKeyDistributionMessage");
563
- return key;
564
- }
674
+ if (content) {
675
+ const keys = Object.keys(content);
676
+ const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
677
+ return key;
678
+ }
565
679
  };
566
- //=======================================================//
680
+ /**
681
+ * Normalizes ephemeral, view once messages to regular message content
682
+ * Eg. image messages in ephemeral messages, in view once messages etc.
683
+ * @param content
684
+ * @returns
685
+ */
567
686
  export const normalizeMessageContent = (content) => {
568
- if (!content) {
569
- return undefined;
570
- }
571
- for (let i = 0; i < 5; i++) {
572
- const inner = getFutureProofMessage(content);
573
- if (!inner) {
574
- break;
575
- }
576
- content = inner.message;
577
- }
578
- return content;
579
- function getFutureProofMessage(message) {
580
- return (message?.ephemeralMessage ||
581
- message?.viewOnceMessage ||
582
- message?.documentWithCaptionMessage ||
583
- message?.viewOnceMessageV2 ||
584
- message?.viewOnceMessageV2Extension ||
585
- message?.editedMessage);
586
- }
687
+ if (!content) {
688
+ return undefined;
689
+ }
690
+ // set max iterations to prevent an infinite loop
691
+ for (let i = 0; i < 5; i++) {
692
+ const inner = getFutureProofMessage(content);
693
+ if (!inner) {
694
+ break;
695
+ }
696
+ content = inner.message;
697
+ }
698
+ return content;
699
+ function getFutureProofMessage(message) {
700
+ return (message?.ephemeralMessage ||
701
+ message?.viewOnceMessage ||
702
+ message?.documentWithCaptionMessage ||
703
+ message?.viewOnceMessageV2 ||
704
+ message?.viewOnceMessageV2Extension ||
705
+ message?.editedMessage);
706
+ }
587
707
  };
588
- //=======================================================//
708
+ /**
709
+ * Extract the true message content from a message
710
+ * Eg. extracts the inner message from a disappearing message/view once message
711
+ */
589
712
  export const extractMessageContent = (content) => {
590
- const extractFromTemplateMessage = (msg) => {
591
- if (msg.imageMessage) {
592
- return { imageMessage: msg.imageMessage };
713
+ const extractFromTemplateMessage = (msg) => {
714
+ if (msg.imageMessage) {
715
+ return { imageMessage: msg.imageMessage };
716
+ }
717
+ else if (msg.documentMessage) {
718
+ return { documentMessage: msg.documentMessage };
719
+ }
720
+ else if (msg.videoMessage) {
721
+ return { videoMessage: msg.videoMessage };
722
+ }
723
+ else if (msg.locationMessage) {
724
+ return { locationMessage: msg.locationMessage };
725
+ }
726
+ else {
727
+ return {
728
+ conversation: 'contentText' in msg ? msg.contentText : 'hydratedContentText' in msg ? msg.hydratedContentText : ''
729
+ };
730
+ }
731
+ };
732
+ content = normalizeMessageContent(content);
733
+ if (content?.buttonsMessage) {
734
+ return extractFromTemplateMessage(content.buttonsMessage);
593
735
  }
594
- else if (msg.documentMessage) {
595
- return { documentMessage: msg.documentMessage };
736
+ if (content?.templateMessage?.hydratedFourRowTemplate) {
737
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedFourRowTemplate);
596
738
  }
597
- else if (msg.videoMessage) {
598
- return { videoMessage: msg.videoMessage };
739
+ if (content?.templateMessage?.hydratedTemplate) {
740
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedTemplate);
599
741
  }
600
- else if (msg.locationMessage) {
601
- return { locationMessage: msg.locationMessage };
742
+ if (content?.templateMessage?.fourRowTemplate) {
743
+ return extractFromTemplateMessage(content?.templateMessage?.fourRowTemplate);
602
744
  }
603
- else {
604
- return {
605
- conversation: "contentText" in msg ? msg.contentText : "hydratedContentText" in msg ? msg.hydratedContentText : ""
606
- };
607
- }
608
- };
609
- content = normalizeMessageContent(content);
610
- if (content?.buttonsMessage) {
611
- return extractFromTemplateMessage(content.buttonsMessage);
612
- }
613
- if (content?.templateMessage?.hydratedFourRowTemplate) {
614
- return extractFromTemplateMessage(content?.templateMessage?.hydratedFourRowTemplate);
615
- }
616
- if (content?.templateMessage?.hydratedTemplate) {
617
- return extractFromTemplateMessage(content?.templateMessage?.hydratedTemplate);
618
- }
619
- if (content?.templateMessage?.fourRowTemplate) {
620
- return extractFromTemplateMessage(content?.templateMessage?.fourRowTemplate);
621
- }
622
- return content;
745
+ return content;
623
746
  };
624
- //=======================================================//
747
+ /**
748
+ * Returns the device predicted by message ID
749
+ */
625
750
  export const getDevice = (id) => /^3A.{18}$/.test(id)
626
- ? "ios"
627
- : /^3E.{20}$/.test(id)
628
- ? "web"
629
- : /^(.{21}|.{32})$/.test(id)
630
- ? "android"
631
- : /^(3F|.{18}$)/.test(id)
632
- ? "desktop"
633
- : "unknown";
634
- //=======================================================//
751
+ ? 'ios'
752
+ : /^3E.{20}$/.test(id)
753
+ ? 'web'
754
+ : /^(.{21}|.{32})$/.test(id)
755
+ ? 'android'
756
+ : /^(3F|.{18}$)/.test(id)
757
+ ? 'desktop'
758
+ : 'unknown';
759
+ /** Upserts a receipt in the message */
635
760
  export const updateMessageWithReceipt = (msg, receipt) => {
636
- msg.userReceipt = msg.userReceipt || [];
637
- const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
638
- if (recp) {
639
- Object.assign(recp, receipt);
640
- }
641
- else {
642
- msg.userReceipt.push(receipt);
643
- }
761
+ msg.userReceipt = msg.userReceipt || [];
762
+ const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
763
+ if (recp) {
764
+ Object.assign(recp, receipt);
765
+ }
766
+ else {
767
+ msg.userReceipt.push(receipt);
768
+ }
644
769
  };
645
- //=======================================================//
770
+ /** Update the message with a new reaction */
646
771
  export const updateMessageWithReaction = (msg, reaction) => {
647
- const authorID = getKeyAuthor(reaction.key);
648
- const reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
649
- reaction.text = reaction.text || "";
650
- reactions.push(reaction);
651
- msg.reactions = reactions;
772
+ const authorID = getKeyAuthor(reaction.key);
773
+ const reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
774
+ reaction.text = reaction.text || '';
775
+ reactions.push(reaction);
776
+ msg.reactions = reactions;
652
777
  };
653
- //=======================================================//
778
+ /** Update the message with a new poll update */
654
779
  export const updateMessageWithPollUpdate = (msg, update) => {
655
- const authorID = getKeyAuthor(update.pollUpdateMessageKey);
656
- const reactions = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
657
- if (update.vote?.selectedOptions?.length) {
658
- reactions.push(update);
659
- }
660
- msg.pollUpdates = reactions;
780
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey);
781
+ const reactions = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
782
+ if (update.vote?.selectedOptions?.length) {
783
+ reactions.push(update);
784
+ }
785
+ msg.pollUpdates = reactions;
661
786
  };
662
- //=======================================================//
787
+ /**
788
+ * Aggregates all poll updates in a poll.
789
+ * @param msg the poll creation message
790
+ * @param meId your jid
791
+ * @returns A list of options & their voters
792
+ */
663
793
  export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
664
- const opts = message?.pollCreationMessage?.options ||
665
- message?.pollCreationMessageV2?.options ||
666
- message?.pollCreationMessageV3?.options ||
667
- [];
668
- const voteHashMap = opts.reduce((acc, opt) => {
669
- const hash = sha256(Buffer.from(opt.optionName || "")).toString();
670
- acc[hash] = {
671
- name: opt.optionName || "",
672
- voters: []
673
- };
674
- return acc;
675
- }, {});
676
- for (const update of pollUpdates || []) {
677
- const { vote } = update;
678
- if (!vote) {
679
- continue;
680
- }
681
- for (const option of vote.selectedOptions || []) {
682
- const hash = option.toString();
683
- let data = voteHashMap[hash];
684
- if (!data) {
685
- voteHashMap[hash] = {
686
- name: "Unknown",
687
- voters: []
794
+ const opts = message?.pollCreationMessage?.options ||
795
+ message?.pollCreationMessageV2?.options ||
796
+ message?.pollCreationMessageV3?.options ||
797
+ [];
798
+ const voteHashMap = opts.reduce((acc, opt) => {
799
+ const hash = sha256(Buffer.from(opt.optionName || '')).toString();
800
+ acc[hash] = {
801
+ name: opt.optionName || '',
802
+ voters: []
688
803
  };
689
- data = voteHashMap[hash];
690
- }
691
- voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
804
+ return acc;
805
+ }, {});
806
+ for (const update of pollUpdates || []) {
807
+ const { vote } = update;
808
+ if (!vote) {
809
+ continue;
810
+ }
811
+ for (const option of vote.selectedOptions || []) {
812
+ const hash = option.toString();
813
+ let data = voteHashMap[hash];
814
+ if (!data) {
815
+ voteHashMap[hash] = {
816
+ name: 'Unknown',
817
+ voters: []
818
+ };
819
+ data = voteHashMap[hash];
820
+ }
821
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
822
+ }
692
823
  }
693
- }
694
- return Object.values(voteHashMap);
824
+ return Object.values(voteHashMap);
695
825
  }
696
- //=======================================================//
826
+ /** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
697
827
  export const aggregateMessageKeysNotFromMe = (keys) => {
698
- const keyMap = {};
699
- for (const { remoteJid, id, participant, fromMe } of keys) {
700
- if (!fromMe) {
701
- const uqKey = `${remoteJid}:${participant || ""}`;
702
- if (!keyMap[uqKey]) {
703
- keyMap[uqKey] = {
704
- jid: remoteJid,
705
- participant: participant,
706
- messageIds: []
707
- };
708
- }
709
- keyMap[uqKey].messageIds.push(id);
828
+ const keyMap = {};
829
+ for (const { remoteJid, id, participant, fromMe } of keys) {
830
+ if (!fromMe) {
831
+ const uqKey = `${remoteJid}:${participant || ''}`;
832
+ if (!keyMap[uqKey]) {
833
+ keyMap[uqKey] = {
834
+ jid: remoteJid,
835
+ participant: participant,
836
+ messageIds: []
837
+ };
838
+ }
839
+ keyMap[uqKey].messageIds.push(id);
840
+ }
710
841
  }
711
- }
712
- return Object.values(keyMap);
842
+ return Object.values(keyMap);
713
843
  };
714
844
  const REUPLOAD_REQUIRED_STATUS = [410, 404];
715
- //=======================================================//
845
+ /**
846
+ * Downloads the given message. Throws an error if it's not a media message
847
+ */
716
848
  export const downloadMediaMessage = async (message, type, options, ctx) => {
717
- const result = await downloadMsg().catch(async (error) => {
718
- if (ctx &&
719
- typeof error?.status === "number" &&
720
- REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
721
- ctx.logger.info({ key: message.key }, "sending reupload media request...");
722
- // request reupload
723
- message = await ctx.reuploadRequest(message);
724
- const result = await downloadMsg();
725
- return result;
726
- }
727
- throw error;
728
- });
729
- return result;
730
- async function downloadMsg() {
731
- const mContent = extractMessageContent(message.message);
732
- if (!mContent) {
733
- throw new Boom("No message present", { statusCode: 400, data: message });
734
- }
735
- const contentType = getContentType(mContent);
736
- let mediaType = contentType?.replace("Message", "");
737
- const media = mContent[contentType];
738
- if (!media || typeof media !== "object" || (!("url" in media) && !("thumbnailDirectPath" in media))) {
739
- throw new Boom(`"${contentType}" message is not a media message`);
740
- }
741
- let download;
742
- if ("thumbnailDirectPath" in media && !("url" in media)) {
743
- download = {
744
- directPath: media.thumbnailDirectPath,
745
- mediaKey: media.mediaKey
746
- };
747
- mediaType = "thumbnail-link";
849
+ const result = await downloadMsg().catch(async (error) => {
850
+ if (ctx &&
851
+ typeof error?.status === 'number' && // treat errors with status as HTTP failures requiring reupload
852
+ REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
853
+ ctx.logger.info({ key: message.key }, 'sending reupload media request...');
854
+ // request reupload
855
+ message = await ctx.reuploadRequest(message);
856
+ const result = await downloadMsg();
857
+ return result;
858
+ }
859
+ throw error;
860
+ });
861
+ return result;
862
+ async function downloadMsg() {
863
+ const mContent = extractMessageContent(message.message);
864
+ if (!mContent) {
865
+ throw new Boom('No message present', { statusCode: 400, data: message });
866
+ }
867
+ const contentType = getContentType(mContent);
868
+ let mediaType = contentType?.replace('Message', '');
869
+ const media = mContent[contentType];
870
+ if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
871
+ throw new Boom(`"${contentType}" message is not a media message`);
872
+ }
873
+ let download;
874
+ if ('thumbnailDirectPath' in media && !('url' in media)) {
875
+ download = {
876
+ directPath: media.thumbnailDirectPath,
877
+ mediaKey: media.mediaKey
878
+ };
879
+ mediaType = 'thumbnail-link';
880
+ }
881
+ else {
882
+ download = media;
883
+ }
884
+ const stream = await downloadContentFromMessage(download, mediaType, options);
885
+ if (type === 'buffer') {
886
+ const bufferArray = [];
887
+ for await (const chunk of stream) {
888
+ bufferArray.push(chunk);
889
+ }
890
+ return Buffer.concat(bufferArray);
891
+ }
892
+ return stream;
748
893
  }
749
- else {
750
- download = media;
751
- }
752
- const stream = await downloadContentFromMessage(download, mediaType, options);
753
- if (type === "buffer") {
754
- const bufferArray = [];
755
- for await (const chunk of stream) {
756
- bufferArray.push(chunk);
757
- }
758
- return Buffer.concat(bufferArray);
759
- }
760
- return stream;
761
- }
762
894
  };
763
- //=======================================================//
895
+ /** Checks whether the given message is a media message; if it is returns the inner content */
764
896
  export const assertMediaContent = (content) => {
765
- content = extractMessageContent(content);
766
- const mediaContent = content?.documentMessage ||
767
- content?.imageMessage ||
768
- content?.videoMessage ||
769
- content?.audioMessage ||
770
- content?.stickerMessage;
771
- if (!mediaContent) {
772
- throw new Boom("given message is not a media message", { statusCode: 400, data: content });
773
- }
774
- return mediaContent;
897
+ content = extractMessageContent(content);
898
+ const mediaContent = content?.documentMessage ||
899
+ content?.imageMessage ||
900
+ content?.videoMessage ||
901
+ content?.audioMessage ||
902
+ content?.stickerMessage;
903
+ if (!mediaContent) {
904
+ throw new Boom('given message is not a media message', { statusCode: 400, data: content });
905
+ }
906
+ return mediaContent;
775
907
  };
776
- //=======================================================//
908
+ //# sourceMappingURL=messages.js.map