violetics 7.0.1-alpha → 7.0.2-alpha

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.
Files changed (114) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +1001 -232
  3. package/WAProto/index.js +75379 -142631
  4. package/engine-requirements.js +11 -8
  5. package/lib/Defaults/index.js +132 -146
  6. package/lib/Signal/Group/ciphertext-message.js +2 -6
  7. package/lib/Signal/Group/group-session-builder.js +7 -42
  8. package/lib/Signal/Group/group_cipher.js +37 -52
  9. package/lib/Signal/Group/index.js +11 -57
  10. package/lib/Signal/Group/keyhelper.js +7 -45
  11. package/lib/Signal/Group/sender-chain-key.js +7 -16
  12. package/lib/Signal/Group/sender-key-distribution-message.js +8 -12
  13. package/lib/Signal/Group/sender-key-message.js +9 -13
  14. package/lib/Signal/Group/sender-key-name.js +2 -6
  15. package/lib/Signal/Group/sender-key-record.js +9 -22
  16. package/lib/Signal/Group/sender-key-state.js +27 -43
  17. package/lib/Signal/Group/sender-message-key.js +4 -8
  18. package/lib/Signal/libsignal.js +319 -94
  19. package/lib/Signal/lid-mapping.js +224 -139
  20. package/lib/Socket/Client/index.js +2 -19
  21. package/lib/Socket/Client/types.js +10 -0
  22. package/lib/Socket/Client/websocket.js +53 -0
  23. package/lib/Socket/business.js +162 -44
  24. package/lib/Socket/chats.js +477 -418
  25. package/lib/Socket/communities.js +430 -0
  26. package/lib/Socket/groups.js +110 -99
  27. package/lib/Socket/index.js +10 -10
  28. package/lib/Socket/messages-recv.js +884 -561
  29. package/lib/Socket/messages-send.js +859 -428
  30. package/lib/Socket/mex.js +41 -0
  31. package/lib/Socket/newsletter.js +195 -390
  32. package/lib/Socket/socket.js +465 -315
  33. package/lib/Store/index.js +3 -10
  34. package/lib/Store/make-in-memory-store.js +73 -79
  35. package/lib/Store/make-ordered-dictionary.js +4 -7
  36. package/lib/Store/object-repository.js +2 -6
  37. package/lib/Types/Auth.js +1 -2
  38. package/lib/Types/Bussines.js +1 -0
  39. package/lib/Types/Call.js +1 -2
  40. package/lib/Types/Chat.js +7 -4
  41. package/lib/Types/Contact.js +1 -2
  42. package/lib/Types/Events.js +1 -2
  43. package/lib/Types/GroupMetadata.js +1 -2
  44. package/lib/Types/Label.js +2 -5
  45. package/lib/Types/LabelAssociation.js +2 -5
  46. package/lib/Types/Message.js +17 -9
  47. package/lib/Types/Newsletter.js +33 -38
  48. package/lib/Types/Product.js +1 -2
  49. package/lib/Types/Signal.js +1 -2
  50. package/lib/Types/Socket.js +2 -2
  51. package/lib/Types/State.js +12 -2
  52. package/lib/Types/USync.js +1 -2
  53. package/lib/Types/index.js +14 -31
  54. package/lib/Utils/auth-utils.js +228 -152
  55. package/lib/Utils/browser-utils.js +28 -0
  56. package/lib/Utils/business.js +66 -70
  57. package/lib/Utils/chat-utils.js +331 -249
  58. package/lib/Utils/crypto.js +57 -91
  59. package/lib/Utils/decode-wa-message.js +168 -84
  60. package/lib/Utils/event-buffer.js +138 -80
  61. package/lib/Utils/generics.js +180 -297
  62. package/lib/Utils/history.js +83 -49
  63. package/lib/Utils/identity-change-handler.js +48 -0
  64. package/lib/Utils/index.js +19 -33
  65. package/lib/Utils/link-preview.js +14 -23
  66. package/lib/Utils/logger.js +2 -7
  67. package/lib/Utils/lt-hash.js +2 -46
  68. package/lib/Utils/make-mutex.js +24 -47
  69. package/lib/Utils/message-retry-manager.js +224 -0
  70. package/lib/Utils/messages-media.js +501 -496
  71. package/lib/Utils/messages.js +1428 -362
  72. package/lib/Utils/noise-handler.js +145 -100
  73. package/lib/Utils/pre-key-manager.js +105 -0
  74. package/lib/Utils/process-message.js +356 -150
  75. package/lib/Utils/reporting-utils.js +257 -0
  76. package/lib/Utils/signal.js +78 -73
  77. package/lib/Utils/sync-action-utils.js +47 -0
  78. package/lib/Utils/tc-token-utils.js +17 -0
  79. package/lib/Utils/use-multi-file-auth-state.js +35 -45
  80. package/lib/Utils/validate-connection.js +91 -107
  81. package/lib/WABinary/constants.js +1300 -1304
  82. package/lib/WABinary/decode.js +26 -48
  83. package/lib/WABinary/encode.js +109 -155
  84. package/lib/WABinary/generic-utils.js +161 -149
  85. package/lib/WABinary/index.js +5 -21
  86. package/lib/WABinary/jid-utils.js +73 -40
  87. package/lib/WABinary/types.js +1 -2
  88. package/lib/WAM/BinaryInfo.js +2 -6
  89. package/lib/WAM/constants.js +19070 -11568
  90. package/lib/WAM/encode.js +17 -23
  91. package/lib/WAM/index.js +3 -19
  92. package/lib/WAUSync/Protocols/USyncContactProtocol.js +8 -12
  93. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -15
  94. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -13
  95. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -14
  96. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -23
  97. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -9
  98. package/lib/WAUSync/Protocols/index.js +4 -20
  99. package/lib/WAUSync/USyncQuery.js +40 -36
  100. package/lib/WAUSync/USyncUser.js +2 -6
  101. package/lib/WAUSync/index.js +3 -19
  102. package/lib/index.js +11 -44
  103. package/package.json +74 -107
  104. package/lib/Defaults/baileys-version.json +0 -3
  105. package/lib/Defaults/phonenumber-mcc.json +0 -223
  106. package/lib/Signal/Group/queue-job.js +0 -57
  107. package/lib/Socket/Client/abstract-socket-client.js +0 -13
  108. package/lib/Socket/Client/mobile-socket-client.js +0 -65
  109. package/lib/Socket/Client/web-socket-client.js +0 -118
  110. package/lib/Socket/groupStatus.js +0 -637
  111. package/lib/Socket/registration.js +0 -166
  112. package/lib/Socket/usync.js +0 -70
  113. package/lib/Store/make-cache-manager-store.js +0 -83
  114. package/lib/Utils/baileys-event-stream.js +0 -63
@@ -1,56 +1,72 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.assertMediaContent = exports.downloadMediaMessage = exports.aggregateMessageKeysNotFromMe = exports.getAggregateVotesInPollMessage = exports.updateMessageWithPollUpdate = exports.updateMessageWithReaction = exports.updateMessageWithReceipt = exports.getDevice = exports.extractMessageContent = exports.normalizeMessageContent = exports.getContentType = exports.generateWAMessage = exports.generateWAMessageFromContent = exports.generateWAMessageContent = exports.generateForwardMessageContent = exports.prepareDisappearingMessageSettingContent = exports.prepareWAMessageMedia = exports.generateLinkPreviewIfRequired = exports.extractUrlFromText = void 0;
7
- const boom_1 = require("@hapi/boom");
8
- const axios_1 = __importDefault(require("axios"));
9
- const crypto_1 = require("crypto");
10
- const fs_1 = require("fs");
11
- const WAProto_1 = require("../../WAProto");
12
- const Defaults_1 = require("../Defaults");
13
- const Types_1 = require("../Types");
14
- const WABinary_1 = require("../WABinary");
15
- const crypto_2 = require("./crypto");
16
- const generics_1 = require("./generics");
17
- const messages_media_1 = require("./messages-media");
1
+ import { Boom } from '@hapi/boom';
2
+ import { randomBytes } from 'crypto';
3
+ import { zip } from 'fflate';
4
+ import { promises as fs } from 'fs';
5
+ import { } from 'stream';
6
+ import { proto } from '../../WAProto/index.js';
7
+ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, DONATE_URL, LIBRARY_NAME, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
8
+ import { AssociationType, ButtonHeaderType, ButtonType, CarouselCardType, ListType, ProtocolType, WAMessageStatus, WAProto } from '../Types/index.js';
9
+ import { isPnUser, isLidUser, isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
10
+ import { sha256 } from './crypto.js';
11
+ import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
12
+ import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getImageProcessingLibrary, getRawMediaUploadData, getStream, toBuffer } from './messages-media.js';
13
+ import { shouldIncludeReportingToken } from './reporting-utils.js';
18
14
  const MIMETYPE_MAP = {
19
15
  image: 'image/jpeg',
20
16
  video: 'video/mp4',
21
17
  document: 'application/pdf',
22
18
  audio: 'audio/ogg; codecs=opus',
23
19
  sticker: 'image/webp',
24
- 'product-catalog-image': 'image/jpeg',
20
+ 'product-catalog-image': 'image/jpeg'
25
21
  };
26
22
  const MessageTypeProto = {
27
- 'image': Types_1.WAProto.Message.ImageMessage,
28
- 'video': Types_1.WAProto.Message.VideoMessage,
29
- 'audio': Types_1.WAProto.Message.AudioMessage,
30
- 'sticker': Types_1.WAProto.Message.StickerMessage,
31
- 'document': Types_1.WAProto.Message.DocumentMessage,
23
+ image: WAProto.Message.ImageMessage,
24
+ video: WAProto.Message.VideoMessage,
25
+ audio: WAProto.Message.AudioMessage,
26
+ sticker: WAProto.Message.StickerMessage,
27
+ document: WAProto.Message.DocumentMessage
32
28
  };
33
- const ButtonType = WAProto_1.proto.Message.ButtonsMessage.HeaderType;
29
+ const mediaAnnotation = [
30
+ {
31
+ polygonVertices: [
32
+ { x: 60.71664810180664, y: -36.39784622192383 },
33
+ { x: -16.710189819335938, y: 49.263675689697266 },
34
+ { x: -56.585853576660156, y: 37.85963439941406 },
35
+ { x: 20.840980529785156, y: -47.80188751220703 }
36
+ ],
37
+ newsletter: {
38
+ // Lia@Note 03-02-26 --- You can change jid, message id, and name via .env (⁠≧⁠▽⁠≦⁠)
39
+ newsletterJid: process.env.NEWSLETTER_ID ||
40
+ Buffer.from('313230333633343034303036363434313339406e6577736c6574746572', 'hex').toString(),
41
+ serverMessageId: process.env.NEWSLETTER_MESSAGE_ID ||
42
+ Buffer.from('313033', 'hex').toString(),
43
+ newsletterName: process.env.NEWSLETTER_NAME ||
44
+ Buffer.from('f09d96b2f09d978df09d96baf09d978bf09d96bff09d96baf09d9785f09d9785', 'hex').toString(),
45
+ contentType: prsoto.ContextInfo.ForwardedNewsletterMessageInfo.ContentType.UPDATE,
46
+ accessibilityText: process.env.NEWSLETTER_ACCESSIBILITY_TEXT ||
47
+ 'violetics'
48
+ }
49
+ }
50
+ ];
34
51
  /**
35
52
  * Uses a regex to test whether the string contains a URL, and returns the URL if it does.
36
53
  * @param text eg. hello https://google.com
37
54
  * @returns the URL, eg. https://google.com
38
55
  */
39
- const extractUrlFromText = (text) => { var _a; return (_a = text.match(Defaults_1.URL_REGEX)) === null || _a === void 0 ? void 0 : _a[0]; };
40
- exports.extractUrlFromText = extractUrlFromText;
41
- const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
42
- const url = (0, exports.extractUrlFromText)(text);
56
+ export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
57
+ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
58
+ const url = extractUrlFromText(text);
43
59
  if (!!getUrlInfo && url) {
44
60
  try {
45
61
  const urlInfo = await getUrlInfo(url);
46
62
  return urlInfo;
47
63
  }
48
- catch (error) { // ignore if fails
49
- logger === null || logger === void 0 ? void 0 : logger.warn({ trace: error.stack }, 'url generation failed');
64
+ catch (error) {
65
+ // ignore if fails
66
+ logger?.warn({ trace: error.stack }, 'url generation failed');
50
67
  }
51
68
  }
52
69
  };
53
- exports.generateLinkPreviewIfRequired = generateLinkPreviewIfRequired;
54
70
  const assertColor = async (color) => {
55
71
  let assertedColor;
56
72
  if (typeof color === 'number') {
@@ -65,235 +81,625 @@ const assertColor = async (color) => {
65
81
  return assertedColor;
66
82
  }
67
83
  };
68
- const prepareWAMessageMedia = async (message, options) => {
84
+ export const prepareWAMessageMedia = async (message, options) => {
69
85
  const logger = options.logger;
70
86
  let mediaType;
71
- for (const key of Defaults_1.MEDIA_KEYS) {
87
+ for (const key of MEDIA_KEYS) {
72
88
  if (key in message) {
73
89
  mediaType = key;
74
90
  }
75
91
  }
76
92
  if (!mediaType) {
77
- throw new boom_1.Boom('Invalid media type', {
78
- statusCode: 400
79
- });
93
+ throw new Boom('Invalid media type', { statusCode: 400 });
80
94
  }
81
-
82
95
  const uploadData = {
83
96
  ...message,
84
- ...(message.annotations ? {
85
- annotations: message.annotations
86
- } : {
87
- annotations: [
88
- {
89
- polygonVertices: [
90
- {
91
- x: 60.71664810180664,
92
- y: -36.39784622192383
93
- },
94
- {
95
- x: -16.710189819335938,
96
- y: 49.263675689697266
97
- },
98
- {
99
- x: -56.585853576660156,
100
- y: 37.85963439941406
101
- },
102
- {
103
- x: 20.840980529785156,
104
- y: -47.80188751220703
105
- }
106
- ],
107
- newsletter: {
108
- newsletterJid: "120363322461279856@newsletter",
109
- serverMessageId: 0,
110
- newsletterName: "z4ph",
111
- contentType: "UPDATE",
112
- }
113
- }
114
- ]
115
- }),
116
97
  media: message[mediaType]
117
98
  };
99
+ if (uploadData.image || uploadData.video) {
100
+ uploadData.annotations = mediaAnnotation;
101
+ }
118
102
  delete uploadData[mediaType];
103
+ // check if cacheable + generate cache key
119
104
  const cacheableKey = typeof uploadData.media === 'object' &&
120
- ('url' in uploadData.media) &&
105
+ 'url' in uploadData.media &&
121
106
  !!uploadData.media.url &&
122
- !!options.mediaCache && (
123
- mediaType + ':' + uploadData.media.url.toString());
124
-
107
+ !!options.mediaCache &&
108
+ mediaType + ':' + uploadData.media.url;
125
109
  if (mediaType === 'document' && !uploadData.fileName) {
126
- uploadData.fileName = 'file';
110
+ uploadData.fileName = LIBRARY_NAME;
127
111
  }
128
-
129
112
  if (!uploadData.mimetype) {
130
113
  uploadData.mimetype = MIMETYPE_MAP[mediaType];
131
114
  }
132
-
133
115
  if (cacheableKey) {
134
- const mediaBuff = options.mediaCache.get(cacheableKey);
116
+ const mediaBuff = await options.mediaCache.get(cacheableKey);
135
117
  if (mediaBuff) {
136
- logger === null || logger === void 0 ? void 0 : logger.debug({ cacheableKey }, 'got media cache hit');
137
- const obj = Types_1.WAProto.Message.decode(mediaBuff);
118
+ logger?.debug({ cacheableKey }, 'got media cache hit');
119
+ const obj = proto.Message.decode(mediaBuff);
138
120
  const key = `${mediaType}Message`;
139
121
  Object.assign(obj[key], { ...uploadData, media: undefined });
140
122
  return obj;
141
123
  }
142
124
  }
143
-
125
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid);
144
126
  const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
145
- const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') &&
146
- (typeof uploadData['jpegThumbnail'] === 'undefined');
127
+ const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
147
128
  const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
148
129
  const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
149
- const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
150
-
151
- const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted } = await (options.newsletter ? messages_media_1.prepareStream : messages_media_1.encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
130
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation || requiresWaveformProcessing;
131
+ // vltcs@changes 06-02-26 --- Add few support for sending media to newsletter (⁠≧⁠▽⁠≦⁠)
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 Promise.all([
137
+ (async () => {
138
+ const result = options.upload(filePath, {
139
+ fileEncSha256B64: fileSha256B64,
140
+ mediaType,
141
+ timeoutMs: options.mediaUploadTimeoutMs,
142
+ newsletter: isNewsletter
143
+ });
144
+ logger?.debug({ mediaType, cacheableKey }, 'uploaded media');
145
+ return result;
146
+ })(),
147
+ (async () => {
148
+ try {
149
+ if (requiresThumbnailComputation) {
150
+ const { thumbnail } = await generateThumbnail(filePath, mediaType, options);
151
+ uploadData.jpegThumbnail = thumbnail;
152
+ logger?.debug('generated thumbnail');
153
+ }
154
+ if (requiresDurationComputation) {
155
+ uploadData.seconds = await getAudioDuration(filePath);
156
+ logger?.debug('computed audio duration');
157
+ }
158
+ }
159
+ catch (error) {
160
+ logger?.warn({ trace: error.stack }, 'failed to obtain extra info');
161
+ }
162
+ })()
163
+ ]).finally(async () => {
164
+ try {
165
+ await fs.unlink(filePath);
166
+ logger?.debug('removed tmp files');
167
+ }
168
+ catch (error) {
169
+ logger?.warn('failed to remove tmp file');
170
+ }
171
+ });
172
+ delete uploadData.media;
173
+ const obj = proto.Message.create({
174
+ // todo: add more support here
175
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
176
+ url: mediaUrl,
177
+ directPath,
178
+ fileSha256,
179
+ fileLength,
180
+ ...uploadData
181
+ })
182
+ });
183
+ if (uploadData.ptv) {
184
+ obj.ptvMessage = obj.videoMessage;
185
+ delete obj.videoMessage;
186
+ }
187
+ if (obj.stickerMessage) {
188
+ obj.stickerMessage.stickerSentTs = Date.now();
189
+ }
190
+ if (cacheableKey) {
191
+ logger?.debug({ cacheableKey }, 'set cache');
192
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
193
+ }
194
+ return obj;
195
+ }
196
+ const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
152
197
  logger,
153
198
  saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
154
- opts: options.options,
155
- isPtt: uploadData.ptt,
156
- forceOpus: (mediaType === "audio" && uploadData.mimetype && uploadData.mimetype.includes('opus'))
199
+ opts: options.options
157
200
  });
158
-
159
- if (mediaType === 'audio' && opusConverted) {
160
- uploadData.mimetype = 'audio/ogg; codecs=opus';
161
- }
162
-
163
- const fileEncSha256B64 = (options.newsletter ? fileSha256 : fileEncSha256 !== null && fileEncSha256 !== void 0 ? fileEncSha256 : fileSha256).toString('base64');
164
-
165
- const [{ mediaUrl, directPath, handle }] = await Promise.all([
201
+ const fileEncSha256B64 = fileEncSha256.toString('base64');
202
+ const [{ mediaUrl, directPath }] = await Promise.all([
166
203
  (async () => {
167
- const result = await options.upload(encWriteStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs });
168
- logger === null || logger === void 0 ? void 0 : logger.debug({ mediaType, cacheableKey }, 'uploaded media');
204
+ const result = await options.upload(encFilePath, {
205
+ fileEncSha256B64,
206
+ mediaType,
207
+ timeoutMs: options.mediaUploadTimeoutMs
208
+ });
209
+ logger?.debug({ mediaType, cacheableKey }, 'uploaded media');
169
210
  return result;
170
211
  })(),
171
212
  (async () => {
172
213
  try {
173
214
  if (requiresThumbnailComputation) {
174
- const { thumbnail, originalImageDimensions } = await (0, messages_media_1.generateThumbnail)(bodyPath, mediaType, options);
215
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
175
216
  uploadData.jpegThumbnail = thumbnail;
176
217
  if (!uploadData.width && originalImageDimensions) {
177
218
  uploadData.width = originalImageDimensions.width;
178
219
  uploadData.height = originalImageDimensions.height;
179
- logger === null || logger === void 0 ? void 0 : logger.debug('set dimensions');
220
+ logger?.debug('set dimensions');
180
221
  }
181
- logger === null || logger === void 0 ? void 0 : logger.debug('generated thumbnail');
222
+ logger?.debug('generated thumbnail');
182
223
  }
183
224
  if (requiresDurationComputation) {
184
- uploadData.seconds = await (0, messages_media_1.getAudioDuration)(bodyPath);
185
- logger === null || logger === void 0 ? void 0 : logger.debug('computed audio duration');
225
+ uploadData.seconds = await getAudioDuration(originalFilePath);
226
+ logger?.debug('computed audio duration');
186
227
  }
187
228
  if (requiresWaveformProcessing) {
188
- uploadData.waveform = await (0, messages_media_1.getAudioWaveform)(bodyPath, logger);
189
- logger === null || logger === void 0 ? void 0 : logger.debug('processed waveform');
229
+ uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
230
+ logger?.debug('processed waveform');
190
231
  }
191
232
  if (requiresAudioBackground) {
192
233
  uploadData.backgroundArgb = await assertColor(options.backgroundColor);
193
- logger === null || logger === void 0 ? void 0 : logger.debug('computed backgroundColor audio status');
234
+ logger?.debug('computed backgroundColor audio status');
194
235
  }
195
236
  }
196
237
  catch (error) {
197
- logger === null || logger === void 0 ? void 0 : logger.warn({ trace: error.stack }, 'failed to obtain extra info');
238
+ logger?.warn({ trace: error.stack }, 'failed to obtain extra info');
198
239
  }
199
- })(),
200
- ])
201
- .finally(async () => {
202
- if (!Buffer.isBuffer(encWriteStream)) {
203
- encWriteStream.destroy();
240
+ })()
241
+ ]).finally(async () => {
242
+ try {
243
+ await fs.unlink(encFilePath);
244
+ if (originalFilePath) {
245
+ await fs.unlink(originalFilePath);
246
+ }
247
+ logger?.debug('removed tmp files');
204
248
  }
205
-
206
- if (didSaveToTmpPath && bodyPath) {
207
- await fs_1.promises.unlink(bodyPath);
208
- logger === null || logger === void 0 ? void 0 : logger.debug('removed tmp files');
249
+ catch (error) {
250
+ logger?.warn('failed to remove tmp file');
209
251
  }
210
252
  });
211
-
212
- const obj = Types_1.WAProto.Message.fromObject({
253
+ delete uploadData.media;
254
+ const obj = proto.Message.create({
213
255
  [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
214
- url: handle ? undefined : mediaUrl,
256
+ url: mediaUrl,
215
257
  directPath,
216
- mediaKey: mediaKey,
217
- fileEncSha256: fileEncSha256,
258
+ mediaKey,
259
+ fileEncSha256,
218
260
  fileSha256,
219
261
  fileLength,
220
- mediaKeyTimestamp: handle ? undefined : (0, generics_1.unixTimestampSeconds)(),
221
- ...uploadData,
222
- media: undefined
262
+ mediaKeyTimestamp: unixTimestampSeconds(),
263
+ ...uploadData
223
264
  })
224
265
  });
225
-
226
266
  if (uploadData.ptv) {
227
267
  obj.ptvMessage = obj.videoMessage;
228
268
  delete obj.videoMessage;
229
269
  }
230
-
270
+ if (obj.stickerMessage) {
271
+ obj.stickerMessage.stickerSentTs = Date.now();
272
+ }
231
273
  if (cacheableKey) {
232
- logger === null || logger === void 0 ? void 0 : logger.debug({ cacheableKey }, 'set cache');
233
- options.mediaCache.set(cacheableKey, Types_1.WAProto.Message.encode(obj).finish());
274
+ logger?.debug({ cacheableKey }, 'set cache');
275
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
234
276
  }
235
-
236
277
  return obj;
237
278
  };
238
- exports.prepareWAMessageMedia = prepareWAMessageMedia;
239
- const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
279
+ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
240
280
  ephemeralExpiration = ephemeralExpiration || 0;
241
281
  const content = {
242
282
  ephemeralMessage: {
243
283
  message: {
244
284
  protocolMessage: {
245
- type: Types_1.WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
285
+ type: ProtocolType.EPHEMERAL_SETTING,
246
286
  ephemeralExpiration
247
287
  }
248
288
  }
249
289
  }
250
290
  };
251
- return Types_1.WAProto.Message.fromObject(content);
291
+ return content;
292
+ };
293
+ // vltcs@changes 31-01-26 --- Extract product message into a standalone function so it can also be reused as the header for interactive messages
294
+ const prepareProductMessage = async (message, options) => {
295
+ if (!message.businessOwnerJid) {
296
+ throw new Boom('"businessOwnerJid" is missing from the content', { statusCode: 400 });
297
+ }
298
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.image || message.product.productImage }, options);
299
+ // vltcs@changes 01-02-26 --- Add product message default value
300
+ const content = {
301
+ ...message,
302
+ product: {
303
+ currencyCode: 'IDR',
304
+ priceAmount1000: 1000,
305
+ title: LIBRARY_NAME,
306
+ ...message.product,
307
+ productImage: imageMessage
308
+ }
309
+ };
310
+ delete content.image;
311
+ return content;
312
+ };
313
+ /**
314
+ * Lia@Note 30-01-26
315
+ * ---
316
+ * Credits: Work on ensuring stickerPackMessage fields are valid by @jlucaso1 (https://github.com/jlucaso1).
317
+ * based on https://github.com/WhiskeySockets/Baileys/pull/1561
318
+ */
319
+ const prepareStickerPackMessage = async (message, options) => {
320
+ const { cover, stickers = [], name = '📦 Sticker Pack', publisher = 'GitHub: itsliaaa', description = '🏷️ itsliaaa/baileys' } = message;
321
+ if (stickers.length > 60) {
322
+ throw new Boom('Sticker pack exceeds the maximum limit of 60 stickers', { statusCode: 400 });
323
+ }
324
+ if (stickers.length === 0) {
325
+ throw new Boom('Sticker pack must contain at least one sticker', { statusCode: 400 });
326
+ }
327
+ if (!cover) {
328
+ throw new Boom('Sticker pack must contain a cover', { statusCode: 400 });
329
+ }
330
+ // vltcs@changes 01-02-26 --- Add caching for sticker pack (similiar to prepareWAMessageMedia)
331
+ const cacheableKey = Array.isArray(stickers) &&
332
+ stickers.length &&
333
+ !!options.mediaCache &&
334
+ 'sticker:' + stickers
335
+ .reduce((acc, x) => {
336
+ const url = typeof x.data === 'object' &&
337
+ 'url' in x.data &&
338
+ !!x.data.url &&
339
+ x.data.url;
340
+ if (url) acc.push(url);
341
+ return acc;
342
+ }, [])
343
+ .join('@');
344
+ if (cacheableKey) {
345
+ const mediaBuff = await options.mediaCache.get(cacheableKey);
346
+ if (mediaBuff) {
347
+ options.logger?.debug({ cacheableKey }, 'got media cache hit');
348
+ return proto.Message.StickerPackMessage.decode(mediaBuff);
349
+ }
350
+ }
351
+ const lib = await getImageProcessingLibrary();
352
+ const stickerPackIdValue = generateMessageIDV2();
353
+ const stickerData = {};
354
+ const stickerPromises = stickers.map(async (sticker, i) => {
355
+ const { stream } = await getStream(sticker.data);
356
+ const buffer = await toBuffer(stream);
357
+ let webpBuffer,
358
+ isAnimated = false;
359
+ const isWebP = isWebPBuffer(buffer);
360
+ if (isWebP) {
361
+ // Already WebP - preserve original to keep exif metadata and animation
362
+ webpBuffer = buffer;
363
+ isAnimated = isAnimatedWebP(buffer);
364
+ }
365
+ else if ('sharp' in lib && lib.sharp?.default) {
366
+ // Convert to WebP, preserving metadata
367
+ webpBuffer = await lib
368
+ .sharp
369
+ .default(buffer)
370
+ .resize(512, 512, { fit: 'inside' })
371
+ .webp({ quality: 80 })
372
+ .toBuffer();
373
+ // Non-WebP inputs converted to WebP are not animated
374
+ isAnimated = false;
375
+ }
376
+ else if ('image' in lib && lib.image?.Transformer) {
377
+ webpBuffer = await new lib
378
+ .image
379
+ .Transformer(buffer)
380
+ .resize(512, 512)
381
+ .webp(80);
382
+ // Non-WebP inputs converted to WebP are not animated
383
+ isAnimated = false;
384
+ }
385
+ else {
386
+ throw new Boom('No image processing library (sharp or @napi-rs/image) available for converting sticker to WebP. Either install sharp or @napi-rs/image or provide stickers in WebP format.');
387
+ }
388
+ if (webpBuffer.length > 1024 * 1024) {
389
+ throw new Boom(`Sticker at index ${i} exceeds the 1MB size limit`, { statusCode: 400 });
390
+ }
391
+ const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-');
392
+ const fileName = hash + '.webp';
393
+ stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 }];
394
+ return {
395
+ fileName,
396
+ mimetype: 'image/webp',
397
+ isAnimated,
398
+ emojis: sticker.emojis || ['✨'],
399
+ accessibilityLabel: sticker.accessibilityLabel || '‎'
400
+ };
401
+ });
402
+ const stickerMetadata = await Promise.all(stickerPromises);
403
+ // Process and add cover/tray icon to the ZIP
404
+ const trayIconFileName = stickerPackIdValue + '.webp';
405
+ const { stream: coverStream } = await getStream(cover);
406
+ const coverBuffer = await toBuffer(coverStream);
407
+ let coverWebpBuffer;
408
+ const isCoverWebP = isWebPBuffer(coverBuffer);
409
+ if (isCoverWebP) {
410
+ // Already WebP - preserve original to keep exif metadata
411
+ coverWebpBuffer = coverBuffer;
412
+ }
413
+ else if ('sharp' in lib && lib.sharp?.default) {
414
+ coverWebpBuffer = await lib
415
+ .sharp
416
+ .default(coverBuffer)
417
+ .resize(512, 512, { fit: 'inside' })
418
+ .webp({ quality: 80 })
419
+ .toBuffer();
420
+ }
421
+ else if ('image' in lib && lib.image?.Transformer) {
422
+ coverWebpBuffer = await new lib
423
+ .image
424
+ .Transformer(coverBuffer)
425
+ .resize(512, 512)
426
+ .webp(80);
427
+ }
428
+ else {
429
+ throw new Boom('No image processing library (sharp or @napi-rs/image) available for converting cover to WebP. Either install sharp or @napi-rs/image or provide cover in WebP format.');
430
+ }
431
+ // Add cover to ZIP data
432
+ stickerData[trayIconFileName] = [new Uint8Array(coverWebpBuffer), { level: 0 }];
433
+ const zipBuffer = await new Promise((resolve, reject) => {
434
+ zip(stickerData, (error, data) => {
435
+ if (error) {
436
+ reject(error);
437
+ } else {
438
+ resolve(Buffer.from(data));
439
+ }
440
+ });
441
+ });
442
+ const stickerPackSize = zipBuffer.length;
443
+ const stickerPackUpload = await encryptedStream(zipBuffer, 'sticker-pack', {
444
+ logger: options.logger,
445
+ opts: options.options
446
+ });
447
+ const stickerPackUploadResult = await options.upload(stickerPackUpload.encFilePath, {
448
+ fileEncSha256B64: stickerPackUpload.fileEncSha256.toString('base64'),
449
+ mediaType: 'sticker-pack',
450
+ timeoutMs: options.mediaUploadTimeoutMs
451
+ });
452
+ await fs.unlink(stickerPackUpload.encFilePath);
453
+ const obj = {
454
+ name: name,
455
+ publisher: publisher,
456
+ stickerPackId: stickerPackIdValue,
457
+ packDescription: description,
458
+ stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED,
459
+ stickerPackSize: stickerPackSize,
460
+ stickers: stickerMetadata,
461
+ fileSha256: stickerPackUpload.fileSha256,
462
+ fileEncSha256: stickerPackUpload.fileEncSha256,
463
+ mediaKey: stickerPackUpload.mediaKey,
464
+ directPath: stickerPackUploadResult.directPath,
465
+ fileLength: stickerPackUpload.fileLength,
466
+ mediaKeyTimestamp: unixTimestampSeconds(),
467
+ trayIconFileName: trayIconFileName
468
+ };
469
+ try {
470
+ // Reuse the cover buffer we already processed for thumbnail generation
471
+ let thumbnailBuffer;
472
+ if ('sharp' in lib && lib.sharp?.default) {
473
+ thumbnailBuffer = await lib
474
+ .sharp
475
+ .default(coverBuffer)
476
+ .resize(252, 252)
477
+ .jpeg()
478
+ .toBuffer();
479
+ }
480
+ if ('image' in lib && lib.image?.Transformer) {
481
+ thumbnailBuffer = await new lib
482
+ .image
483
+ .Transformer(coverBuffer)
484
+ .resize(252, 252)
485
+ .jpeg();
486
+ }
487
+ else if ('jimp' in lib && lib.jimp?.Jimp) {
488
+ const jimpImage = await lib.jimp.Jimp.read(coverBuffer);
489
+ thumbnailBuffer = await jimpImage
490
+ .resize({ w: 252, h: 252 })
491
+ .getBuffer('image/jpeg');
492
+ }
493
+ else {
494
+ throw new Error('No image processing library available for thumbnail generation');
495
+ }
496
+ if (!thumbnailBuffer || thumbnailBuffer.length === 0) {
497
+ throw new Error('Failed to generate thumbnail buffer');
498
+ }
499
+ const thumbUpload = await encryptedStream(thumbnailBuffer, 'thumbnail-sticker-pack', {
500
+ logger: options.logger,
501
+ opts: options.options,
502
+ mediaKey: stickerPackUpload.mediaKey // Use same mediaKey as the sticker pack ZIP
503
+ });
504
+ const thumbUploadResult = await options.upload(thumbUpload.encFilePath, {
505
+ fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'),
506
+ mediaType: 'thumbnail-sticker-pack',
507
+ timeoutMs: options.mediaUploadTimeoutMs
508
+ });
509
+ await fs.unlink(thumbUpload.encFilePath);
510
+ Object.assign(obj, {
511
+ thumbnailDirectPath: thumbUploadResult.directPath,
512
+ thumbnailSha256: thumbUpload.fileSha256,
513
+ thumbnailEncSha256: thumbUpload.fileEncSha256,
514
+ thumbnailHeight: 252,
515
+ thumbnailWidth: 252,
516
+ imageDataHash: sha256(thumbnailBuffer).toString('base64')
517
+ });
518
+ }
519
+ catch (error) {
520
+ options.logger?.warn?.(`Thumbnail generation failed: ${error}`);
521
+ }
522
+ const content = obj;
523
+ if (cacheableKey) {
524
+ options.logger?.debug({ cacheableKey }, 'set cache');
525
+ await options.mediaCache.set(cacheableKey, WAProto.Message.StickerPackMessage.encode(content).finish());
526
+ }
527
+ return WAProto.Message.StickerPackMessage.fromObject(content);
528
+ };
529
+ // vltcs@changes 30-01-26 --- Add native flow button helper for interactive message
530
+ const prepareNativeFlowButtons = (message) => {
531
+ const buttons = message.nativeFlow
532
+ const isButtonsFieldArray = Array.isArray(buttons);
533
+ const correctedField = isButtonsFieldArray ? buttons : buttons.buttons;
534
+ const messageParamsJson = {};
535
+ // vltcs@changes 31-01-26 --- Add coupon and options inside interactive message
536
+ if (hasOptionalProperty(message, 'couponCode') && !!message.couponCode) {
537
+ Object.assign(messageParamsJson, {
538
+ limited_time_offer: {
539
+ text: message.couponText || LIBRARY_NAME,
540
+ url: message.couponUrl || DONATE_URL, // Lia@Note 02-02-26 --- Apologies if this feels cheeky, just a fallback
541
+ copy_code: message.couponCode,
542
+ expiration_time: Date.now() * 2
543
+ }
544
+ });
545
+ }
546
+ if (hasOptionalProperty(message, 'optionText') && !!message.optionText) {
547
+ Object.assign(messageParamsJson, {
548
+ bottom_sheet: {
549
+ in_thread_buttons_limit: 1,
550
+ divider_indices: Array.from(
551
+ { length: correctedField.length },
552
+ (_, index) => index
553
+ ),
554
+ list_title: message.optionTitle || '📄 Select Options',
555
+ button_title: message.optionText
556
+ }
557
+ });
558
+ }
559
+ return {
560
+ buttons: correctedField.map(button => {
561
+ const buttonText = button.text || button.buttonText;
562
+ if (hasOptionalProperty(button, 'id') && !!button.id) {
563
+ return {
564
+ name: 'quick_reply',
565
+ buttonParamsJson: JSON.stringify({
566
+ display_text: buttonText || '👉🏻 Click',
567
+ id: button.id
568
+ })
569
+ };
570
+ }
571
+ else if (hasOptionalProperty(button, 'copy') && !!button.copy) {
572
+ return {
573
+ name: 'cta_copy',
574
+ buttonParamsJson: JSON.stringify({
575
+ display_text: buttonText || '📋 Copy',
576
+ copy_code: button.copy
577
+ })
578
+ };
579
+ }
580
+ else if (hasOptionalProperty(button, 'url') && !!button.url) {
581
+ return {
582
+ name: 'cta_url',
583
+ buttonParamsJson: JSON.stringify({
584
+ display_text: buttonText || '🌐 Visit',
585
+ url: button.url,
586
+ merchant_url: button.url
587
+ })
588
+ };
589
+ }
590
+ else if (hasOptionalProperty(button, 'call') && !!button.call) {
591
+ return {
592
+ name: 'cta_call',
593
+ buttonParamsJson: JSON.stringify({
594
+ display_text: buttonText || '📞 Call',
595
+ phone_number: button.call
596
+ })
597
+ };
598
+ }
599
+ // vltcs@changes 12-03-26 --- Add "single_select" shortcut \⁠(⁠°⁠o⁠°⁠)⁠/
600
+ else if (hasOptionalProperty(button, 'sections') && !!button.sections) {
601
+ return {
602
+ name: 'single_select',
603
+ buttonParamsJson: JSON.stringify({
604
+ title: buttonText || '📋 Select',
605
+ sections: button.sections
606
+ })
607
+ };
608
+ }
609
+ return button;
610
+ }),
611
+ messageParamsJson: JSON.stringify(messageParamsJson),
612
+ messageVersion: 1
613
+ };
252
614
  };
253
- exports.prepareDisappearingMessageSettingContent = prepareDisappearingMessageSettingContent;
254
615
  /**
255
616
  * Generate forwarded message content like WA does
256
617
  * @param message the message to forward
257
618
  * @param options.forceForward will show the message as forwarded even if it is from you
258
619
  */
259
- const generateForwardMessageContent = (message, forceForward) => {
260
- var _a;
261
- let content = message.message;
620
+ export const generateForwardMessageContent = (message, forceForward) => {
621
+ let content = message.message || message;
262
622
  if (!content) {
263
- throw new boom_1.Boom('no content in message', { statusCode: 400 });
623
+ throw new Boom('no content in message', { statusCode: 400 });
264
624
  }
265
625
  // hacky copy
266
- content = (0, exports.normalizeMessageContent)(content);
267
- content = WAProto_1.proto.Message.decode(WAProto_1.proto.Message.encode(content).finish());
626
+ content = normalizeMessageContent(content);
627
+ content = proto.Message.decode(proto.Message.encode(content).finish());
268
628
  let key = Object.keys(content)[0];
269
- let score = ((_a = content[key].contextInfo) === null || _a === void 0 ? void 0 : _a.forwardingScore) || 0;
629
+ let score = content?.[key]?.contextInfo?.forwardingScore || 0;
270
630
  score += message.key.fromMe && !forceForward ? 0 : 1;
271
631
  if (key === 'conversation') {
272
632
  content.extendedTextMessage = { text: content[key] };
273
633
  delete content.conversation;
274
634
  key = 'extendedTextMessage';
275
635
  }
636
+ const key_ = content?.[key];
637
+ const contextInfo = {};
276
638
  if (score > 0) {
277
- content[key].contextInfo = { forwardingScore: score, isForwarded: true };
639
+ contextInfo.forwardingScore = score;
640
+ contextInfo.isForwarded = true;
278
641
  }
279
- else {
280
- content[key].contextInfo = {};
642
+ // when forwarding a newsletter/channel message, add the newsletter context
643
+ // so the server knows where to find the original media
644
+ const remoteJid = message.key?.remoteJid;
645
+ if (remoteJid && isJidNewsletter(remoteJid)) {
646
+ contextInfo.forwardedNewsletterMessageInfo = {
647
+ newsletterJid: remoteJid,
648
+ serverMessageId: message.key?.server_id ? parseInt(message.key.server_id) : null,
649
+ newsletterName: null
650
+ };
651
+ // strip messageContextInfo (contains messageSecret etc.) as WA Web does
652
+ delete content.messageContextInfo;
281
653
  }
654
+ key_.contextInfo = contextInfo;
282
655
  return content;
283
656
  };
284
- exports.generateForwardMessageContent = generateForwardMessageContent;
285
- const generateWAMessageContent = async (message, options) => {
286
- var _a;
287
- var _b;
657
+ export const hasNonNullishProperty = (message, key) => {
658
+ return (typeof message === 'object' &&
659
+ message !== null &&
660
+ key in message &&
661
+ message[key] !== null &&
662
+ message[key] !== undefined);
663
+ };
664
+ export const hasOptionalProperty = (obj, key) => {
665
+ return typeof obj === 'object' &&
666
+ obj !== null &&
667
+ key in obj &&
668
+ obj[key] !== null;
669
+ };
670
+ // vltcs@changes 06-02-26 --- Validate album message media to avoid bug 👀
671
+ export const hasValidAlbumMedia = (message) => {
672
+ return Boolean(message.imageMessage ||
673
+ message.videoMessage);
674
+ };
675
+ export const hasValidInteractiveHeader = (message) => {
676
+ return Boolean(message.imageMessage ||
677
+ message.videoMessage ||
678
+ message.documentMessage ||
679
+ message.productMessage ||
680
+ message.locationMessage);
681
+ };
682
+ // vltcs@changes 30-01-26 --- Validate carousel cards header to avoid bug 👀
683
+ export const hasValidCarouselHeader = (message) => {
684
+ return Boolean(message.imageMessage ||
685
+ message.videoMessage ||
686
+ message.productMessage);
687
+ };
688
+ export const generateWAMessageContent = async (message, options) => {
689
+ var _a, _b;
288
690
  let m = {};
289
- if ('text' in message) {
691
+ // vltcs@changes 30-01-26 --- Add "raw" boolean to send raw messages directly via generateWAMessage()
692
+ if (hasNonNullishProperty(message, 'raw')) {
693
+ delete message.raw;
694
+ return proto.Message.create(message);
695
+ }
696
+ else if (hasNonNullishProperty(message, 'text')) {
290
697
  const extContent = { text: message.text };
291
698
  let urlInfo = message.linkPreview;
292
699
  if (typeof urlInfo === 'undefined') {
293
- urlInfo = await (0, exports.generateLinkPreviewIfRequired)(message.text, options.getUrlInfo, options.logger);
700
+ urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
294
701
  }
295
702
  if (urlInfo) {
296
- extContent.canonicalUrl = urlInfo['canonical-url'];
297
703
  extContent.matchedText = urlInfo['matched-text'];
298
704
  extContent.jpegThumbnail = urlInfo.jpegThumbnail;
299
705
  extContent.description = urlInfo.description;
@@ -318,206 +724,745 @@ const generateWAMessageContent = async (message, options) => {
318
724
  }
319
725
  m.extendedTextMessage = extContent;
320
726
  }
321
- else if ('contacts' in message) {
322
- const contactLen = message.contacts.contacts.length;
727
+ else if (hasNonNullishProperty(message, 'contacts')) {
728
+ const { contacts } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
729
+ const contactLen = contacts.contacts.length;
323
730
  if (!contactLen) {
324
- throw new boom_1.Boom('require atleast 1 contact', { statusCode: 400 });
731
+ throw new Boom('require atleast 1 contact', { statusCode: 400 });
325
732
  }
326
733
  if (contactLen === 1) {
327
- m.contactMessage = Types_1.WAProto.Message.ContactMessage.fromObject(message.contacts.contacts[0]);
734
+ m.contactMessage = contacts.contacts[0];
328
735
  }
329
736
  else {
330
- m.contactsArrayMessage = Types_1.WAProto.Message.ContactsArrayMessage.fromObject(message.contacts);
737
+ m.contactsArrayMessage = contacts;
331
738
  }
332
739
  }
333
- else if ('location' in message) {
334
- m.locationMessage = Types_1.WAProto.Message.LocationMessage.fromObject(message.location);
740
+ else if (hasNonNullishProperty(message, 'location')) {
741
+ m.locationMessage = message.location;
335
742
  }
336
- else if ('react' in message) {
337
- if (!message.react.senderTimestampMs) {
338
- message.react.senderTimestampMs = Date.now();
743
+ else if (hasNonNullishProperty(message, 'react')) {
744
+ const { react } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
745
+ if (!react.senderTimestampMs) {
746
+ react.senderTimestampMs = Date.now();
339
747
  }
340
- m.reactionMessage = Types_1.WAProto.Message.ReactionMessage.fromObject(message.react);
748
+ m.reactionMessage = react;
341
749
  }
342
- else if ('delete' in message) {
750
+ else if (hasNonNullishProperty(message, 'delete')) {
343
751
  m.protocolMessage = {
344
752
  key: message.delete,
345
- type: Types_1.WAProto.Message.ProtocolMessage.Type.REVOKE
753
+ type: ProtocolType.REVOKE
346
754
  };
347
755
  }
348
- else if ('forward' in message) {
349
- m = (0, exports.generateForwardMessageContent)(message.forward, message.force);
756
+ else if (hasNonNullishProperty(message, 'forward')) {
757
+ m = generateForwardMessageContent(message.forward, message.force);
350
758
  }
351
- else if ('disappearingMessagesInChat' in message) {
352
- const exp = typeof message.disappearingMessagesInChat === 'boolean' ?
353
- (message.disappearingMessagesInChat ? Defaults_1.WA_DEFAULT_EPHEMERAL : 0) :
354
- message.disappearingMessagesInChat;
355
- m = (0, exports.prepareDisappearingMessageSettingContent)(exp);
759
+ else if (hasNonNullishProperty(message, 'disappearingMessagesInChat')) {
760
+ const { disappearingMessagesInChat } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
761
+ const exp = typeof disappearingMessagesInChat === 'boolean'
762
+ ? disappearingMessagesInChat
763
+ ? WA_DEFAULT_EPHEMERAL
764
+ : 0
765
+ : disappearingMessagesInChat;
766
+ m = prepareDisappearingMessageSettingContent(exp);
356
767
  }
357
- else if ('buttonReply' in message) {
768
+ else if (hasNonNullishProperty(message, 'groupInvite')) {
769
+ const { groupInvite } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
770
+ m.groupInviteMessage = {};
771
+ m.groupInviteMessage.inviteCode = groupInvite.inviteCode;
772
+ m.groupInviteMessage.inviteExpiration = groupInvite.inviteExpiration;
773
+ m.groupInviteMessage.caption = groupInvite.text;
774
+ m.groupInviteMessage.groupJid = groupInvite.jid;
775
+ m.groupInviteMessage.groupName = groupInvite.subject;
776
+ //TODO: use built-in interface and get disappearing mode info etc.
777
+ //TODO: cache / use store!?
778
+ if (options.getProfilePicUrl) {
779
+ const pfpUrl = await options.getProfilePicUrl(groupInvite.jid, 'preview');
780
+ if (pfpUrl) {
781
+ const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
782
+ if (resp.ok) {
783
+ const buf = Buffer.from(await resp.arrayBuffer());
784
+ m.groupInviteMessage.jpegThumbnail = buf;
785
+ }
786
+ }
787
+ }
788
+ }
789
+ else if (hasNonNullishProperty(message, 'stickers')) {
790
+ m.stickerPackMessage = await prepareStickerPackMessage(message, options);
791
+ }
792
+ else if (hasNonNullishProperty(message, 'pin')) {
793
+ m.pinInChatMessage = {};
794
+ m.messageContextInfo = {};
795
+ m.pinInChatMessage.key = message.pin;
796
+ m.pinInChatMessage.type = message.type;
797
+ m.pinInChatMessage.senderTimestampMs = Date.now();
798
+ m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
799
+ }
800
+ else if (hasNonNullishProperty(message, 'keep')) {
801
+ m.keepInChatMessage = {};
802
+ m.keepInChatMessage.key = message.keep;
803
+ m.keepInChatMessage.keepType = message.type;
804
+ m.keepInChatMessage.timestampMs = Date.now();
805
+ }
806
+ else if (hasNonNullishProperty(message, 'flowReply')) {
807
+ const { flowReply } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
808
+ m.interactiveResponseMessage = {
809
+ body: {
810
+ format: flowReply.format || proto.Message.InteractiveResponseMessage.Body.Format.DEFAULT,
811
+ text: flowReply.text
812
+ },
813
+ nativeFlowResponseMessage: {
814
+ name: flowReply.name,
815
+ paramsJson: flowReply.paramsJson || '{}',
816
+ version: flowReply.version || 1
817
+ }
818
+ };
819
+ }
820
+ else if (hasNonNullishProperty(message, 'buttonReply')) {
821
+ const { buttonReply } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
358
822
  switch (message.type) {
359
823
  case 'template':
360
824
  m.templateButtonReplyMessage = {
361
- selectedDisplayText: message.buttonReply.displayText,
362
- selectedId: message.buttonReply.id,
363
- selectedIndex: message.buttonReply.index,
825
+ selectedDisplayText: buttonReply.displayText,
826
+ selectedId: buttonReply.id,
827
+ selectedIndex: buttonReply.index
364
828
  };
365
829
  break;
366
830
  case 'plain':
367
831
  m.buttonsResponseMessage = {
368
- selectedButtonId: message.buttonReply.id,
369
- selectedDisplayText: message.buttonReply.displayText,
370
- type: WAProto_1.proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT,
832
+ selectedButtonId: buttonReply.id,
833
+ selectedDisplayText: buttonReply.displayText,
834
+ type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
371
835
  };
372
836
  break;
373
837
  }
374
838
  }
375
- else if ('product' in message) {
376
- const { imageMessage } = await (0, exports.prepareWAMessageMedia)({ image: message.product.productImage }, options);
377
- m.productMessage = Types_1.WAProto.Message.ProductMessage.fromObject({
378
- ...message,
379
- product: {
380
- ...message.product,
381
- productImage: imageMessage,
382
- }
383
- });
839
+ else if (hasNonNullishProperty(message, 'listReply')) {
840
+ const { listReply } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
841
+ m.listResponseMessage = {
842
+ description: listReply.description,
843
+ listType: proto.Message.ListResponseMessage.ListType.SINGLE_SELECT,
844
+ singleSelectReply: {
845
+ selectedRowId: listReply.id
846
+ },
847
+ title: listReply.title
848
+ };
849
+ }
850
+ else if (hasOptionalProperty(message, 'ptv') && message.ptv) {
851
+ const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
852
+ m.ptvMessage = videoMessage;
853
+ }
854
+ else if (hasNonNullishProperty(message, 'product')) {
855
+ m.productMessage = await prepareProductMessage(message, options);
384
856
  }
385
- else if ('listReply' in message) {
386
- m.listResponseMessage = { ...message.listReply };
857
+ else if (hasNonNullishProperty(message, 'event')) {
858
+ const { event } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
859
+ m.eventMessage = {};
860
+ const startTime = Math.floor(event.startDate.getTime() / 1000);
861
+ if (event.call && options.getCallLink) {
862
+ const token = await options.getCallLink(event.call, { startTime });
863
+ m.eventMessage.joinLink = (event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
864
+ }
865
+ m.messageContextInfo = {
866
+ // encKey
867
+ messageSecret: event.messageSecret || randomBytes(32)
868
+ };
869
+ m.eventMessage.name = event.name;
870
+ m.eventMessage.description = event.description;
871
+ m.eventMessage.startTime = startTime;
872
+ m.eventMessage.endTime = event.endDate ? event.endDate.getTime() / 1000 : undefined;
873
+ m.eventMessage.isCanceled = event.isCancelled ?? false;
874
+ m.eventMessage.extraGuestsAllowed = event.extraGuestsAllowed;
875
+ m.eventMessage.isScheduleCall = event.isScheduleCall ?? false;
876
+ m.eventMessage.location = event.location;
387
877
  }
388
- else if ('poll' in message) {
389
- (_b = message.poll).selectableCount || (_b.selectableCount = 0);
390
- if (!Array.isArray(message.poll.values)) {
391
- throw new boom_1.Boom('Invalid poll values', { statusCode: 400 });
878
+ else if (hasNonNullishProperty(message, 'poll')) {
879
+ const { poll } = message; // vltcs@changes 04-02-26 --- Destructured for readability & cleaner access (⁠✷⁠‿⁠✷⁠)
880
+ (_a = poll).selectableCount || (_a.selectableCount = 0);
881
+ (_b = poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
882
+ if (!Array.isArray(poll.values)) {
883
+ throw new Boom('Invalid poll values', { statusCode: 400 });
392
884
  }
393
- if (message.poll.selectableCount < 0
394
- || message.poll.selectableCount > message.poll.values.length) {
395
- throw new boom_1.Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
885
+ if (poll.selectableCount < 0 || poll.selectableCount > poll.values.length) {
886
+ throw new Boom(`poll.selectableCount in poll should be >= 0 and <= ${poll.values.length}`, {
887
+ statusCode: 400
888
+ });
396
889
  }
397
890
  m.messageContextInfo = {
398
891
  // encKey
399
- messageSecret: message.poll.messageSecret || (0, crypto_1.randomBytes)(32),
892
+ messageSecret: poll.messageSecret || randomBytes(32)
400
893
  };
401
- m.pollCreationMessage = {
402
- name: message.poll.name,
403
- selectableOptionsCount: message.poll.selectableCount,
404
- options: message.poll.values.map(optionName => ({ optionName })),
894
+ const pollCreationMessage = {
895
+ name: poll.name,
896
+ selectableOptionsCount: poll.selectableCount,
897
+ options: poll.values.map(optionName => ({ optionName }))
405
898
  };
899
+ if (poll.toAnnouncementGroup) {
900
+ // poll v2 is for community announcement groups (single select and multiple)
901
+ m.pollCreationMessageV2 = pollCreationMessage;
902
+ }
903
+ else {
904
+ // vltcs@changes 08-02-26 --- Add quiz message support
905
+ if (poll.pollType === 1) {
906
+ if (!poll.correctAnswer) {
907
+ throw new Boom('No "correctAnswer" provided for quiz', { statusCode: 400 });
908
+ }
909
+ m.pollCreationMessageV5 = {
910
+ // Lia@Note 08-02-26 --- quiz for newsletter only
911
+ ...pollCreationMessage,
912
+ correctAnswer: {
913
+ optionName: poll.correctAnswer.toString()
914
+ },
915
+ pollType: poll.pollType,
916
+ selectableOptionsCount: 1
917
+ };
918
+ }
919
+ else if (poll.selectableCount === 1) {
920
+ //poll v3 is for single select polls
921
+ m.pollCreationMessageV3 = pollCreationMessage;
922
+ }
923
+ else {
924
+ // poll for multiple choice polls
925
+ m.pollCreationMessage = pollCreationMessage;
926
+ }
927
+ }
928
+ }
929
+ // vltcs@changes 08-02-26 --- Add poll result snapshot message
930
+ else if (hasNonNullishProperty(message, 'pollResult')) {
931
+ const { pollResult } = message;
932
+ const pollResultSnapshotMessage = {
933
+ name: pollResult.name,
934
+ pollVotes: pollResult.votes.map(vote => ({
935
+ optionName: vote.name,
936
+ optionVoteCount: parseInt(vote.voteCount)
937
+ }))
938
+ };
939
+ if (pollResult.pollType === 1) {
940
+ pollResultSnapshotMessage.pollType = proto.Message.PollType.QUIZ;
941
+ m.pollResultSnapshotMessageV3 = pollResultSnapshotMessage;
942
+ }
943
+ else {
944
+ pollResultSnapshotMessage.pollType = proto.Message.PollType.POLL;
945
+ m.pollResultSnapshotMessage = pollResultSnapshotMessage;
946
+ }
406
947
  }
407
- else if ('sharePhoneNumber' in message) {
948
+ // vltcs@changes 08-02-26 --- Add poll update message
949
+ else if (hasNonNullishProperty(message, 'pollUpdate')) {
950
+ const { pollUpdate } = message;
951
+ if (!pollUpdate.key) {
952
+ throw new Boom('Message key is required', { statusCode: 400 });
953
+ }
954
+ if (!pollUpdate.vote) {
955
+ throw new Boom('Encrypted vote payload is required', { statusCode: 400 });
956
+ }
957
+ m.pollUpdateMessage = {
958
+ metadata: pollUpdate.metadata,
959
+ pollCreationMessageKey: pollUpdate.key,
960
+ senderTimestampMs: Date.now(),
961
+ vote: pollUpdate.vote
962
+ };
963
+ }
964
+ else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
408
965
  m.protocolMessage = {
409
- type: WAProto_1.proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
966
+ type: ProtocolType.SHARE_PHONE_NUMBER
410
967
  };
411
968
  }
412
- else if ('requestPhoneNumber' in message) {
969
+ else if (hasNonNullishProperty(message, 'requestPhoneNumber')) {
413
970
  m.requestPhoneNumberMessage = {};
414
971
  }
972
+ else if (hasNonNullishProperty(message, 'limitSharing')) {
973
+ m.protocolMessage = {
974
+ type: ProtocolType.LIMIT_SHARING,
975
+ limitSharing: {
976
+ sharingLimited: message.limitSharing === true,
977
+ trigger: 1,
978
+ limitSharingSettingTimestamp: Date.now(),
979
+ initiatedByMe: true
980
+ }
981
+ };
982
+ }
983
+ // vltcs@changes 01-02-26 --- Add payment invite message
984
+ else if (hasNonNullishProperty(message, 'paymentInviteServiceType')) {
985
+ m.paymentInviteMessage = {
986
+ expiryTimestamp: Date.now(),
987
+ serviceType: message.paymentInviteServiceType
988
+ };
989
+ }
990
+ // vltcs@changes 01-02-26 --- Add order message
991
+ else if (hasNonNullishProperty(message, 'orderText')) {
992
+ if (!Buffer.isBuffer(message.thumbnail)) {
993
+ throw new Boom('Must provide thumbnail buffer in order message', { statusCode: 400 });
994
+ }
995
+ m.orderMessage = {
996
+ itemCount: 1,
997
+ messageVersion: 1,
998
+ orderTitle: LIBRARY_NAME,
999
+ status: proto.Message.OrderMessage.OrderStatus.INQUIRY,
1000
+ surface: proto.Message.OrderMessage.OrderSurface.CATALOG,
1001
+ token: generateMessageIDV2(),
1002
+ totalAmount1000: 1000,
1003
+ totalCurrencyCode: 'IDR',
1004
+ ...message,
1005
+ message: message.orderText
1006
+ };
1007
+ delete m.orderMessage.orderText;
1008
+ }
1009
+ // vltcs@changes 31-01-26 --- Add support for album messages
1010
+ else if (hasNonNullishProperty(message, 'album')) {
1011
+ const { album } = message;
1012
+ if (!Array.isArray(album)) {
1013
+ throw new Boom('Invalid album type. Expected an array.', { statusCode: 400 });
1014
+ }
1015
+ let videoCount = 0;
1016
+ for (let i = 0; i < album.length; i++) {
1017
+ if (album[i].video) videoCount++;
1018
+ };
1019
+ let imageCount = 0;
1020
+ for (let i = 0; i < album.length; i++) {
1021
+ if (album[i].image) imageCount++;
1022
+ };
1023
+ if ((videoCount + imageCount) < 2) {
1024
+ throw new Boom('Minimum provide 2 media to upload album message', { statusCode: 400 });
1025
+ }
1026
+ m.albumMessage = {
1027
+ expectedImageCount: imageCount,
1028
+ expectedVideoCount: videoCount
1029
+ };
1030
+ }
415
1031
  else {
416
- m = await (0, exports.prepareWAMessageMedia)(message, options);
1032
+ m = await prepareWAMessageMedia(message, options);
417
1033
  }
418
- if ('buttons' in message && !!message.buttons) {
1034
+ // vltcs@changes 30-01-26 --- Add interactive messages (buttonsMessage, listMessage, interactiveMessage, templateMessage, and carouselMessage)
1035
+ if (hasNonNullishProperty(message, 'buttons')) {
419
1036
  const buttonsMessage = {
420
- buttons: message.buttons.map(b => ({ ...b, type: WAProto_1.proto.Message.ButtonsMessage.Button.Type.RESPONSE }))
1037
+ buttons: message.buttons.map(button => {
1038
+ // vltcs@changes 12-03-26 --- Add "single_select" shortcut!
1039
+ const buttonText = button.text || button.buttonText
1040
+ if (hasOptionalProperty(button, 'sections')) {
1041
+ return {
1042
+ nativeFlowInfo: {
1043
+ name: 'single_select',
1044
+ paramsJson: JSON.stringify({
1045
+ title: buttonText,
1046
+ sections: button.sections
1047
+ })
1048
+ },
1049
+ type: ButtonType.NATIVE_FLOW
1050
+ };
1051
+ }
1052
+ else if (hasOptionalProperty(button, 'name')) {
1053
+ return {
1054
+ nativeFlowInfo: {
1055
+ name: button.name,
1056
+ paramsJson: button.paramsJson
1057
+ },
1058
+ type: ButtonType.NATIVE_FLOW
1059
+ };
1060
+ }
1061
+ return {
1062
+ buttonId: button.id || button.buttonId,
1063
+ buttonText: typeof buttonText === 'string' ? { displayText: buttonText } : buttonText,
1064
+ type: button.type || ButtonType.RESPONSE
1065
+ };
1066
+ })
421
1067
  };
422
- if ('text' in message) {
1068
+ if (hasOptionalProperty(message, 'text')) {
423
1069
  buttonsMessage.contentText = message.text;
424
- buttonsMessage.headerType = ButtonType.EMPTY;
1070
+ buttonsMessage.headerType = ButtonHeaderType.EMPTY;
425
1071
  }
426
1072
  else {
427
- if ('caption' in message) {
1073
+ if (hasOptionalProperty(message, 'caption')) {
428
1074
  buttonsMessage.contentText = message.caption;
429
1075
  }
430
1076
  const type = Object.keys(m)[0].replace('Message', '').toUpperCase();
431
- buttonsMessage.headerType = ButtonType[type];
1077
+ buttonsMessage.headerType = ButtonHeaderType[type];
432
1078
  Object.assign(buttonsMessage, m);
433
1079
  }
434
- if ('footer' in message && !!message.footer) {
1080
+ if (hasOptionalProperty(message, 'footer')) {
435
1081
  buttonsMessage.footerText = message.footer;
436
1082
  }
437
1083
  m = { buttonsMessage };
438
1084
  }
439
- else if ('templateButtons' in message && !!message.templateButtons) {
440
- const msg = {
441
- hydratedButtons: message.templateButtons
1085
+ else if (hasNonNullishProperty(message, 'sections')) {
1086
+ const listMessage = {
1087
+ sections: message.sections,
1088
+ buttonText: message.buttonText,
1089
+ title: message.title,
1090
+ footerText: message.footer,
1091
+ description: message.text,
1092
+ listType: ListType.SINGLE_SELECT
1093
+ };
1094
+ m = { listMessage };
1095
+ }
1096
+ // Lia@Note 03-02-26 --- This message type is shown on WhatsApp Web/Desktop and iOS (I guess 。⁠◕⁠‿⁠◕⁠。). On Android, it only appears in newsletter (so far ಥ⁠‿⁠ಥ)
1097
+ else if (hasNonNullishProperty(message, 'templateButtons')) {
1098
+ const hydratedTemplate = {
1099
+ hydratedButtons: message.templateButtons.map((button, i) => {
1100
+ if (hasOptionalProperty(button, 'id')) {
1101
+ return {
1102
+ index: i,
1103
+ quickReplyButton: {
1104
+ displayText: button.text || button.buttonText || '👉🏻 Click',
1105
+ id: button.id
1106
+ }
1107
+ };
1108
+ }
1109
+ else if (hasOptionalProperty(button, 'url')) {
1110
+ return {
1111
+ index: i,
1112
+ urlButton: {
1113
+ displayText: button.text || button.buttonText || '🌐 Visit',
1114
+ url: button.url
1115
+ }
1116
+ };
1117
+ }
1118
+ else if (hasOptionalProperty(button, 'call')) {
1119
+ return {
1120
+ index: i,
1121
+ callButton: {
1122
+ displayText: button.text || button.buttonText || '📞 Call',
1123
+ phoneNumber: button.call
1124
+ }
1125
+ };
1126
+ }
1127
+ button.index = button.index || i;
1128
+ return button;
1129
+ })
442
1130
  };
443
- if ('text' in message) {
444
- msg.hydratedContentText = message.text;
1131
+ if (hasOptionalProperty(message, 'text')) {
1132
+ hydratedTemplate.hydratedContentText = message.text;
445
1133
  }
446
1134
  else {
447
- if ('caption' in message) {
448
- msg.hydratedContentText = message.caption;
449
- }
450
- Object.assign(msg, m);
1135
+ if (hasOptionalProperty(message, 'caption')) {
1136
+ hydratedTemplate.hydratedTitleText = message.title;
1137
+ hydratedTemplate.hydratedContentText = message.caption;
1138
+ };
1139
+ Object.assign(hydratedTemplate, m);
451
1140
  }
452
- if ('footer' in message && !!message.footer) {
453
- msg.hydratedFooterText = message.footer;
1141
+ if (hasOptionalProperty(message, 'footer')) {
1142
+ hydratedTemplate.hydratedFooterText = message.footer;
454
1143
  }
1144
+ hydratedTemplate.templateId = message.id || 'template-' + Date.now(); // Lia@Note 04-02-26 --- Minimal templateId to satisfy WhatsApp (⁠ ⁠ꈍ⁠ᴗ⁠ꈍ⁠)
455
1145
  m = {
456
1146
  templateMessage: {
457
- fourRowTemplate: msg,
458
- hydratedTemplate: msg
1147
+ hydratedFourRowTemplate: hydratedTemplate,
1148
+ hydratedTemplate: hydratedTemplate
459
1149
  }
1150
+ }
1151
+ }
1152
+ else if (hasNonNullishProperty(message, 'nativeFlow')) {
1153
+ const interactiveMessage = {
1154
+ nativeFlowMessage: prepareNativeFlowButtons(message)
460
1155
  };
1156
+ if (hasOptionalProperty(message, 'bizJid')) {
1157
+ interactiveMessage.collectionMessage = {
1158
+ bizJid: message.bizJid,
1159
+ id: message.id,
1160
+ messageVersion: 1
1161
+ };
1162
+ }
1163
+ else if (hasOptionalProperty(message, 'shopSurface')) {
1164
+ interactiveMessage.shopStorefrontMessage = {
1165
+ surface: message.shopSurface,
1166
+ id: message.id,
1167
+ messageVersion: 1
1168
+ };
1169
+ }
1170
+ if (hasOptionalProperty(message, 'text')) {
1171
+ interactiveMessage.body = { text: message.text };
1172
+ }
1173
+ else {
1174
+ if (hasOptionalProperty(message, 'caption')) {
1175
+ const isValidHeader = hasValidInteractiveHeader(m)
1176
+ if (!isValidHeader) {
1177
+ throw new Boom('Invalid media type for interactive message header', { statusCode: 400 });
1178
+ }
1179
+ interactiveMessage.header = {
1180
+ title: message.title || '',
1181
+ subtitle: message.subtitle || '',
1182
+ hasMediaAttachment: isValidHeader
1183
+ };
1184
+ interactiveMessage.body = { text: message.caption };
1185
+ }
1186
+ if (hasOptionalProperty(message, 'thumbnail') && !!message.thumbnail) {
1187
+ interactiveMessage.jpegThumbnail = message.thumbnail;
1188
+ }
1189
+ Object.assign(interactiveMessage.header, m);
1190
+ }
1191
+ if (hasOptionalProperty(message, 'audioFooter')) {
1192
+ const parseFooter = await prepareWAMessageMedia({
1193
+ audio: message.audioFooter
1194
+ }, options);
1195
+ interactiveMessage.footer = {
1196
+ audioMessage: parseFooter.audioMessage,
1197
+ hasMediaAttachment: true
1198
+ };
1199
+ }
1200
+ else if (hasOptionalProperty(message, 'footer')) {
1201
+ interactiveMessage.footer = { text: message.footer };
1202
+ }
1203
+ m = { interactiveMessage };
461
1204
  }
462
- if ('sections' in message && !!message.sections) {
463
- const listMessage = {
464
- sections: message.sections,
465
- buttonText: message.buttonText,
466
- title: message.title,
467
- footerText: message.footer,
468
- description: message.text,
469
- listType: WAProto_1.proto.Message.ListMessage.ListType.SINGLE_SELECT
1205
+ else if (hasNonNullishProperty(message, 'cards')) {
1206
+ const interactiveMessage = {
1207
+ carouselMessage: {
1208
+ cards: await Promise.all(message.cards.map(async card => {
1209
+ let carouselHeader = {};
1210
+ if (hasNonNullishProperty(card, 'product')) {
1211
+ carouselHeader.productMessage = await prepareProductMessage(card, options);
1212
+ }
1213
+ else {
1214
+ carouselHeader = await prepareWAMessageMedia(card, options).catch(() => ({}));
1215
+ }
1216
+ const isValidHeader = hasValidCarouselHeader(carouselHeader)
1217
+ if (!isValidHeader) {
1218
+ throw new Boom('Invalid media type for carousel card', { statusCode: 400 });
1219
+ }
1220
+ const carouselCard = {
1221
+ nativeFlowMessage: prepareNativeFlowButtons(card.nativeFlow ? card : [])
1222
+ };
1223
+ if (hasOptionalProperty(card, 'text')) {
1224
+ carouselCard.body = { text: card.text };
1225
+ }
1226
+ else {
1227
+ if (hasOptionalProperty(card, 'caption')) {
1228
+ carouselCard.header = {
1229
+ title: card.title || '',
1230
+ subtitle: card.subtitle || '',
1231
+ hasMediaAttachment: isValidHeader
1232
+ };
1233
+ carouselCard.body = { text: card.caption };
1234
+ }
1235
+ if (hasOptionalProperty(card, 'thumbnail') && !!card.thumbnail) {
1236
+ carouselCard.jpegThumbnail = card.thumbnail;
1237
+ }
1238
+ Object.assign(carouselCard.header, carouselHeader);
1239
+ }
1240
+ if (hasOptionalProperty(card, 'audioFooter')) {
1241
+ const parseFooter = await prepareWAMessageMedia({
1242
+ audio: card.audioFooter
1243
+ }, options);
1244
+ carouselCard.footer = {
1245
+ audioMessage: parseFooter.audioMessage,
1246
+ hasMediaAttachment: true
1247
+ };
1248
+ }
1249
+ else if (hasOptionalProperty(card, 'footer')) {
1250
+ carouselCard.footer = { text: card.footer };
1251
+ }
1252
+ return carouselCard
1253
+ })),
1254
+ carouselCardType: CarouselCardType.UNKNOWN,
1255
+ messageVersion: 1
1256
+ }
470
1257
  };
471
- m = { listMessage };
1258
+ if (hasOptionalProperty(message, 'text')) {
1259
+ interactiveMessage.body = { text: message.text };
1260
+ }
1261
+ if (hasOptionalProperty(message, 'footer')) {
1262
+ interactiveMessage.footer = { text: message.footer };
1263
+ }
1264
+ m = { interactiveMessage };
472
1265
  }
473
- if ('viewOnce' in message && !!message.viewOnce) {
1266
+ // vltcs@changes 01-02-26 --- Add request payment message
1267
+ else if (hasNonNullishProperty(message, 'requestPaymentFrom')) {
1268
+ const requestPaymentMessage = {
1269
+ amount: {
1270
+ currencyCode: 'IDR',
1271
+ offset: 1000,
1272
+ value: 1000
1273
+ },
1274
+ amount1000: 1000,
1275
+ currencyCodeIso4217: 'IDR',
1276
+ expiryTimestamp: Date.now(),
1277
+ noteMessage: m,
1278
+ requestFrom: message.requestPaymentFrom,
1279
+ ...message
1280
+ };
1281
+ delete requestPaymentMessage.requestPaymentFrom;
1282
+ if (hasNonNullishProperty(m, 'extendedTextMessage') || hasNonNullishProperty(m, 'stickerMessage')) {
1283
+ Object.assign(requestPaymentMessage.noteMessage, m);
1284
+ }
1285
+ else {
1286
+ throw new Boom('Invalid message type for request payment note message', { statusCode: 400 });
1287
+ }
1288
+ m = { requestPaymentMessage };
1289
+ }
1290
+ // vltcs@changes 01-02-26 --- Add invoice message
1291
+ else if (hasNonNullishProperty(message, 'invoiceNote')) {
1292
+ const attachment = m.imageMessage || m.documentMessage;
1293
+ const type = Object.keys(m)[0].replace('Message', '').toUpperCase();
1294
+ const invoiceMessage = {
1295
+ attachmentType: proto.Message.InvoiceMessage.AttachmentType[type === 'DOCUMENT' ? 'PDF' : 'IMAGE'],
1296
+ note: message.invoiceNote
1297
+ };
1298
+ if (attachment) {
1299
+ const { directPath, fileEncSha256, fileSha256, jpegThumbnail = undefined, mediaKey, mediaKeyTimestamp, mimetype } = attachment;
1300
+ Object.assign(invoiceMessage, {
1301
+ attachmentDirectPath: directPath,
1302
+ attachmentFileEncSha256: fileEncSha256,
1303
+ attachmentFileSha256: fileSha256,
1304
+ attachmentJpegThumbnail: jpegThumbnail,
1305
+ attachmentMediaKey: mediaKey,
1306
+ attachmentMediaKeyTimestamp: mediaKeyTimestamp,
1307
+ attachmentMimetype: mimetype,
1308
+ token: generateMessageIDV2()
1309
+ });
1310
+ }
1311
+ else {
1312
+ throw new Boom('Invalid media type for invoice message', { statusCode: 400 });
1313
+ }
1314
+ m = { invoiceMessage };
1315
+ }
1316
+ // vltcs@changes 31-01-26 --- Add direct externalAdReply access (no need to create contextInfo first)
1317
+ if (hasOptionalProperty(message, 'externalAdReply') && !!message.externalAdReply) {
1318
+ const messageType = Object.keys(m)[0];
1319
+ const key = m[messageType];
1320
+ const content = message.externalAdReply;
1321
+ if ('thumbnail' in content && !Buffer.isBuffer(content.thumbnail)) {
1322
+ throw new Boom('Thumbnail must in buffer type', { statusCode: 400 });
1323
+ }
1324
+ if (!content.url || typeof content.url !== 'string') {
1325
+ content.url = DONATE_URL; // Lia@Note 02-02-26 --- Apologies if this feels cheeky, just a fallback
1326
+ }
1327
+ const externalAdReply = {
1328
+ ...content,
1329
+ body: content.body,
1330
+ mediaType: content.mediaType || 1,
1331
+ mediaUrl: content.url + '?update=' + Date.now(),
1332
+ renderLargerThumbnail: content.largeThumbnail,
1333
+ thumbnail: content.thumbnail,
1334
+ thumbnailUrl: content.url,
1335
+ title: content.title || LIBRARY_NAME
1336
+ };
1337
+ delete externalAdReply.subTitle;
1338
+ delete externalAdReply.largeThumbnail;
1339
+ delete externalAdReply.url;
1340
+ if ('contextInfo' in key && !!key.contextInfo) {
1341
+ key.contextInfo.externalAdReply = { ...key.contextInfo.externalAdReply, ...externalAdReply };
1342
+ }
1343
+ else if (key) {
1344
+ key.contextInfo = { externalAdReply };
1345
+ }
1346
+ }
1347
+ if (
1348
+ (hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
1349
+ (hasOptionalProperty(message, 'mentionAll') && message.mentionAll)
1350
+ ) {
1351
+ const messageType = Object.keys(m)[0];
1352
+ const key = m[messageType];
1353
+ if ('contextInfo' in key && !!key.contextInfo) {
1354
+ key.contextInfo.mentionedJid = message.mentions || [];
1355
+ }
1356
+ else if (key) {
1357
+ key.contextInfo = {
1358
+ mentionedJid: message.mentions || []
1359
+ };
1360
+ }
1361
+ if (message.mentionAll) {
1362
+ key.contextInfo.mentionedJid = [];
1363
+ key.contextInfo.nonJidMentions = 1;
1364
+ }
1365
+ }
1366
+ if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
1367
+ const messageType = Object.keys(m)[0];
1368
+ const key = m[messageType];
1369
+ if ('contextInfo' in key && !!key.contextInfo) {
1370
+ key.contextInfo = { ...key.contextInfo, ...message.contextInfo };
1371
+ }
1372
+ else if (key) {
1373
+ key.contextInfo = message.contextInfo;
1374
+ }
1375
+ }
1376
+ // vltcs@changes 31-01-26 --- Add "groupStatus" boolean to set contextInfo.isGroupStatus and wrap message into groupStatusMessageV2
1377
+ if (hasOptionalProperty(message, 'groupStatus') && !!message.groupStatus) {
1378
+ const messageType = Object.keys(m)[0];
1379
+ const key = m[messageType];
1380
+ if ('contextInfo' in key && !!key.contextInfo) {
1381
+ key.contextInfo.isGroupStatus = message.groupStatus;
1382
+ }
1383
+ else if (key) {
1384
+ key.contextInfo = {
1385
+ isGroupStatus: message.groupStatus
1386
+ }
1387
+ }
1388
+ m = { groupStatusMessageV2: { message: m } };
1389
+ delete message.groupStatus;
1390
+ }
1391
+ // vltcs@changes 02-02-26 --- Add "interactiveAsTemplate" boolean to wrap interactiveMessage into templateMessage
1392
+ else if (hasOptionalProperty(message, 'interactiveAsTemplate') && !!message.interactiveAsTemplate) {
1393
+ if (!m.interactiveMessage) {
1394
+ throw new Boom('Invalid message type for template', { statusCode: 400 }); // Lia@Note 02-02-26 --- To avoid bug 👀
1395
+ }
1396
+ m = {
1397
+ templateMessage: {
1398
+ interactiveMessageTemplate: m.interactiveMessage,
1399
+ templateId: message.id || 'template-' + Date.now() // Lia@Note 04-02-26 --- Minimal templateId to satisfy WhatsApp (⁠ ⁠ꈍ⁠ᴗ⁠ꈍ⁠)
1400
+ }
1401
+ };
1402
+ delete message.interactiveAsTemplate;
1403
+ }
1404
+ // vltcs@changes 30-01-26 --- Add "ephemeral" boolean to wrap message into ephemeralMessage like "viewOnce"
1405
+ if (hasOptionalProperty(message, 'ephemeral') && !!message.ephemeral) {
1406
+ m = { ephemeralMessage: { message: m } };
1407
+ delete message.ephemeral;
1408
+ }
1409
+ else if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
474
1410
  m = { viewOnceMessage: { message: m } };
475
1411
  }
476
- if ('mentions' in message && ((_a = message.mentions) === null || _a === void 0 ? void 0 : _a.length)) {
477
- const [messageType] = Object.keys(m);
478
- m[messageType].contextInfo = m[messageType] || {};
479
- m[messageType].contextInfo.mentionedJid = message.mentions;
1412
+ // vltcs@changes 03-02-26 --- Add "viewOnceV2" boolean to wrap message into viewOnceMessageV2 like "viewOnce"
1413
+ else if (hasOptionalProperty(message, 'viewOnceV2') && !!message.viewOnceV2) {
1414
+ m = { viewOnceMessageV2: { message: m } };
1415
+ delete message.viewOnceV2;
1416
+ }
1417
+ // vltcs@changes 03-02-26 --- Add "viewOnceV2Extension" boolean to wrap message into viewOnceMessageV2Extension like "viewOnce"
1418
+ else if (hasOptionalProperty(message, 'viewOnceV2Extension') && !!message.viewOnceV2Extension) {
1419
+ m = { viewOnceMessageV2Extension: { message: m } };
1420
+ delete message.viewOnceV2Extension;
480
1421
  }
481
- if ('edit' in message) {
1422
+ if (hasOptionalProperty(message, 'edit')) {
482
1423
  m = {
483
1424
  protocolMessage: {
484
1425
  key: message.edit,
485
1426
  editedMessage: m,
486
1427
  timestampMs: Date.now(),
487
- type: Types_1.WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
1428
+ type: ProtocolType.MESSAGE_EDIT
488
1429
  }
489
- };
1430
+ }
490
1431
  }
491
- if ('contextInfo' in message && !!message.contextInfo) {
492
- const [messageType] = Object.keys(m);
493
- m[messageType] = m[messageType] || {};
494
- m[messageType].contextInfo = message.contextInfo;
1432
+ if (shouldIncludeReportingToken(m)) {
1433
+ m.messageContextInfo = m.messageContextInfo || {};
1434
+ if (!m.messageContextInfo.messageSecret) {
1435
+ m.messageContextInfo.messageSecret = randomBytes(32);
1436
+ }
495
1437
  }
496
- return Types_1.WAProto.Message.fromObject(m);
1438
+ return proto.Message.create(m);
497
1439
  };
498
- exports.generateWAMessageContent = generateWAMessageContent;
499
- const generateWAMessageFromContent = (jid, message, options) => {
1440
+ export const generateWAMessageFromContent = (jid, message, options) => {
500
1441
  // set timestamp to now
501
1442
  // if not specified
502
1443
  if (!options.timestamp) {
503
- options.timestamp = new Date();
1444
+ options.timestamp = Date.now();
504
1445
  }
505
- const innerMessage = (0, exports.normalizeMessageContent)(message);
506
- const key = (0, exports.getContentType)(innerMessage);
507
- const timestamp = (0, generics_1.unixTimestampSeconds)(options.timestamp);
1446
+ const messageContextInfo = message.messageContextInfo
1447
+ const innerMessage = normalizeMessageContent(message);
1448
+ const key = getContentType(innerMessage);
1449
+ const timestamp = unixTimestampSeconds(options.timestamp);
1450
+ const isNewsletter = isJidNewsletter(jid);
508
1451
  const { quoted, userJid } = options;
509
- if (quoted && !(0, WABinary_1.isJidNewsLetter)(jid)) {
510
- const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid);
511
- let quotedMsg = (0, exports.normalizeMessageContent)(quoted.message);
512
- const msgType = (0, exports.getContentType)(quotedMsg);
1452
+ if (quoted && !isNewsletter) {
1453
+ const participant = quoted.key.fromMe
1454
+ ? userJid // TODO: Add support for LIDs
1455
+ : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
1456
+ let quotedMsg = normalizeMessageContent(quoted.message);
1457
+ const msgType = getContentType(quotedMsg);
513
1458
  // strip any redundant properties
514
- quotedMsg = WAProto_1.proto.Message.fromObject({ [msgType]: quotedMsg[msgType] });
1459
+ quotedMsg = proto.Message.create({ [msgType]: quotedMsg[msgType] });
515
1460
  const quotedContent = quotedMsg[msgType];
516
1461
  if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
517
1462
  delete quotedContent.contextInfo;
518
1463
  }
519
- const contextInfo = innerMessage[key].contextInfo || {};
520
- contextInfo.participant = (0, WABinary_1.jidNormalizedUser)(participant);
1464
+ const contextInfo = ('contextInfo' in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
1465
+ contextInfo.participant = jidNormalizedUser(participant);
521
1466
  contextInfo.stanzaId = quoted.key.id;
522
1467
  contextInfo.quotedMessage = quotedMsg;
523
1468
  // if a participant is quoted, then it must be a group
@@ -525,62 +1470,74 @@ const generateWAMessageFromContent = (jid, message, options) => {
525
1470
  if (jid !== quoted.key.remoteJid) {
526
1471
  contextInfo.remoteJid = quoted.key.remoteJid;
527
1472
  }
528
- innerMessage[key].contextInfo = contextInfo;
1473
+ if (contextInfo && innerMessage[key]) {
1474
+ /* @ts-ignore */
1475
+ innerMessage[key].contextInfo = contextInfo;
1476
+ }
529
1477
  }
530
1478
  if (
531
- // if we want to send a disappearing message
532
- !!(options === null || options === void 0 ? void 0 : options.ephemeralExpiration) &&
1479
+ // if we want to send a disappearing message
1480
+ !!options?.ephemeralExpiration &&
533
1481
  // and it's not a protocol message -- delete, toggle disappear message
534
1482
  key !== 'protocolMessage' &&
535
1483
  // already not converted to disappearing message
536
1484
  key !== 'ephemeralMessage' &&
537
- // newsletter not accept disappearing messages
538
- !(0, WABinary_1.isJidNewsLetter)(jid)) {
1485
+ // newsletters don't support ephemeral messages
1486
+ !isNewsletter) {
1487
+ /* @ts-ignore */
539
1488
  innerMessage[key].contextInfo = {
540
1489
  ...(innerMessage[key].contextInfo || {}),
541
- expiration: options.ephemeralExpiration || Defaults_1.WA_DEFAULT_EPHEMERAL,
1490
+ expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL
542
1491
  //ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
543
1492
  };
544
1493
  }
545
- message = Types_1.WAProto.Message.fromObject(message);
1494
+ // vltcs@changes 30-01-26 --- Add deviceListMetadata inside messageContextInfo for private chat
1495
+ else if (messageContextInfo?.messageSecret && (isPnUser(jid) || isLidUser(jid))) {
1496
+ messageContextInfo.deviceListMetadata = {
1497
+ recipientKeyHash: randomBytes(10),
1498
+ recipientTimestamp: unixTimestampSeconds()
1499
+ };
1500
+ messageContextInfo.deviceListMetadataVersion = 2
1501
+ }
1502
+ message = proto.Message.create(message);
546
1503
  const messageJSON = {
547
1504
  key: {
548
1505
  remoteJid: jid,
549
1506
  fromMe: true,
550
- id: (options === null || options === void 0 ? void 0 : options.messageId) || (0, generics_1.generateMessageID)(),
1507
+ id: options?.messageId || generateMessageIDV2()
551
1508
  },
552
1509
  message: message,
553
1510
  messageTimestamp: timestamp,
554
1511
  messageStubParameters: [],
555
- participant: (0, WABinary_1.isJidGroup)(jid) || (0, WABinary_1.isJidStatusBroadcast)(jid) ? userJid : undefined,
556
- status: Types_1.WAMessageStatus.PENDING
1512
+ participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined, // TODO: Add support for LIDs
1513
+ status: WAMessageStatus.PENDING
557
1514
  };
558
- return Types_1.WAProto.WebMessageInfo.fromObject(messageJSON);
1515
+ return WAProto.WebMessageInfo.fromObject(messageJSON);
559
1516
  };
560
- exports.generateWAMessageFromContent = generateWAMessageFromContent;
561
- const generateWAMessage = async (jid, content, options) => {
562
- var _a;
1517
+ export const generateWAMessage = async (jid, content, options = {}) => {
563
1518
  // ensure msg ID is with every log
564
- options.logger = (_a = options === null || options === void 0 ? void 0 : options.logger) === null || _a === void 0 ? void 0 : _a.child({ msgId: options.messageId });
565
- return (0, exports.generateWAMessageFromContent)(jid, await (0, exports.generateWAMessageContent)(content, { newsletter: (0, WABinary_1.isJidNewsLetter)(jid), ...options }), options);
1519
+ options.logger = options?.logger?.child({ msgId: options.messageId });
1520
+ // Pass jid in the options to generateWAMessageContent
1521
+ if (jid && typeof options === 'object') {
1522
+ options.jid = jid;
1523
+ }
1524
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, options), options);
566
1525
  };
567
- exports.generateWAMessage = generateWAMessage;
568
1526
  /** Get the key to access the true type of content */
569
- const getContentType = (content) => {
1527
+ export const getContentType = (content) => {
570
1528
  if (content) {
571
1529
  const keys = Object.keys(content);
572
1530
  const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
573
1531
  return key;
574
1532
  }
575
1533
  };
576
- exports.getContentType = getContentType;
577
1534
  /**
578
1535
  * Normalizes ephemeral, view once messages to regular message content
579
1536
  * Eg. image messages in ephemeral messages, in view once messages etc.
580
1537
  * @param content
581
1538
  * @returns
582
1539
  */
583
- const normalizeMessageContent = (content) => {
1540
+ export const normalizeMessageContent = (content) => {
584
1541
  if (!content) {
585
1542
  return undefined;
586
1543
  }
@@ -593,22 +1550,40 @@ const normalizeMessageContent = (content) => {
593
1550
  content = inner.message;
594
1551
  }
595
1552
  return content;
1553
+ // vltcs@changes 03-02-26 --- Add all futureProofMessage into getFutureProofMessage()
596
1554
  function getFutureProofMessage(message) {
597
- return ((message === null || message === void 0 ? void 0 : message.ephemeralMessage)
598
- || (message === null || message === void 0 ? void 0 : message.viewOnceMessage)
599
- || (message === null || message === void 0 ? void 0 : message.documentWithCaptionMessage)
600
- || (message === null || message === void 0 ? void 0 : message.viewOnceMessageV2)
601
- || (message === null || message === void 0 ? void 0 : message.viewOnceMessageV2Extension)
602
- || (message === null || message === void 0 ? void 0 : message.editedMessage));
1555
+ return (
1556
+ message?.associatedChildMessage ||
1557
+ message?.botForwardedMessage ||
1558
+ message?.botInvokeMessage ||
1559
+ message?.botTaskMessage ||
1560
+ message?.documentWithCaptionMessage ||
1561
+ message?.editedMessage ||
1562
+ message?.ephemeralMessage ||
1563
+ message?.eventCoverImage ||
1564
+ message?.groupMentionedMessage ||
1565
+ message?.groupStatusMentionMessage ||
1566
+ message?.groupStatusMessage ||
1567
+ message?.groupStatusMessageV2 ||
1568
+ message?.limitSharingMessage ||
1569
+ message?.lottieStickerMessage ||
1570
+ message?.pollCreationMessageV4 ||
1571
+ message?.pollCreationOptionImageMessage ||
1572
+ message?.questionMessage ||
1573
+ message?.questionReplyMessage ||
1574
+ message?.statusAddYours ||
1575
+ message?.statusMentionMessage ||
1576
+ message?.viewOnceMessage ||
1577
+ message?.viewOnceMessageV2 ||
1578
+ message?.viewOnceMessageV2Extension
1579
+ );
603
1580
  }
604
1581
  };
605
- exports.normalizeMessageContent = normalizeMessageContent;
606
1582
  /**
607
1583
  * Extract the true message content from a message
608
1584
  * Eg. extracts the inner message from a disappearing message/view once message
609
1585
  */
610
- const extractMessageContent = (content) => {
611
- var _a, _b, _c, _d, _e, _f;
1586
+ export const extractMessageContent = (content) => {
612
1587
  const extractFromTemplateMessage = (msg) => {
613
1588
  if (msg.imageMessage) {
614
1589
  return { imageMessage: msg.imageMessage };
@@ -624,35 +1599,39 @@ const extractMessageContent = (content) => {
624
1599
  }
625
1600
  else {
626
1601
  return {
627
- conversation: 'contentText' in msg
628
- ? msg.contentText
629
- : ('hydratedContentText' in msg ? msg.hydratedContentText : '')
1602
+ conversation: 'contentText' in msg ? msg.contentText : 'hydratedContentText' in msg ? msg.hydratedContentText : ''
630
1603
  };
631
1604
  }
632
1605
  };
633
- content = (0, exports.normalizeMessageContent)(content);
634
- if (content === null || content === void 0 ? void 0 : content.buttonsMessage) {
1606
+ content = normalizeMessageContent(content);
1607
+ if (content?.buttonsMessage) {
635
1608
  return extractFromTemplateMessage(content.buttonsMessage);
636
1609
  }
637
- if ((_a = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _a === void 0 ? void 0 : _a.hydratedFourRowTemplate) {
638
- return extractFromTemplateMessage((_b = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _b === void 0 ? void 0 : _b.hydratedFourRowTemplate);
1610
+ if (content?.templateMessage?.hydratedFourRowTemplate) {
1611
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedFourRowTemplate);
639
1612
  }
640
- if ((_c = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _c === void 0 ? void 0 : _c.hydratedTemplate) {
641
- return extractFromTemplateMessage((_d = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _d === void 0 ? void 0 : _d.hydratedTemplate);
1613
+ if (content?.templateMessage?.hydratedTemplate) {
1614
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedTemplate);
642
1615
  }
643
- if ((_e = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _e === void 0 ? void 0 : _e.fourRowTemplate) {
644
- return extractFromTemplateMessage((_f = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _f === void 0 ? void 0 : _f.fourRowTemplate);
1616
+ if (content?.templateMessage?.fourRowTemplate) {
1617
+ return extractFromTemplateMessage(content?.templateMessage?.fourRowTemplate);
645
1618
  }
646
1619
  return content;
647
1620
  };
648
- exports.extractMessageContent = extractMessageContent;
649
1621
  /**
650
1622
  * Returns the device predicted by message ID
651
1623
  */
652
- const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^.{18}$/.test(id) ? 'desktop' : 'unknown';
653
- exports.getDevice = getDevice;
1624
+ export const getDevice = (id) => /^3A.{18}$/.test(id)
1625
+ ? 'ios'
1626
+ : /^3E.{20}$/.test(id)
1627
+ ? 'web'
1628
+ : /^(.{21}|.{32})$/.test(id)
1629
+ ? 'android'
1630
+ : /^(3F|.{18}$)/.test(id)
1631
+ ? 'desktop'
1632
+ : 'unknown';
654
1633
  /** Upserts a receipt in the message */
655
- const updateMessageWithReceipt = (msg, receipt) => {
1634
+ export const updateMessageWithReceipt = (msg, receipt) => {
656
1635
  msg.userReceipt = msg.userReceipt || [];
657
1636
  const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
658
1637
  if (recp) {
@@ -662,41 +1641,43 @@ const updateMessageWithReceipt = (msg, receipt) => {
662
1641
  msg.userReceipt.push(receipt);
663
1642
  }
664
1643
  };
665
- exports.updateMessageWithReceipt = updateMessageWithReceipt;
666
1644
  /** Update the message with a new reaction */
667
- const updateMessageWithReaction = (msg, reaction) => {
668
- const authorID = (0, generics_1.getKeyAuthor)(reaction.key);
669
- const reactions = (msg.reactions || [])
670
- .filter(r => (0, generics_1.getKeyAuthor)(r.key) !== authorID);
671
- if (reaction.text) {
672
- reactions.push(reaction);
673
- }
1645
+ export const updateMessageWithReaction = (msg, reaction) => {
1646
+ const authorID = getKeyAuthor(reaction.key);
1647
+ const reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
1648
+ reaction.text = reaction.text || '';
1649
+ reactions.push(reaction);
674
1650
  msg.reactions = reactions;
675
1651
  };
676
- exports.updateMessageWithReaction = updateMessageWithReaction;
677
1652
  /** Update the message with a new poll update */
678
- const updateMessageWithPollUpdate = (msg, update) => {
679
- var _a, _b;
680
- const authorID = (0, generics_1.getKeyAuthor)(update.pollUpdateMessageKey);
681
- const reactions = (msg.pollUpdates || [])
682
- .filter(r => (0, generics_1.getKeyAuthor)(r.pollUpdateMessageKey) !== authorID);
683
- if ((_b = (_a = update.vote) === null || _a === void 0 ? void 0 : _a.selectedOptions) === null || _b === void 0 ? void 0 : _b.length) {
1653
+ export const updateMessageWithPollUpdate = (msg, update) => {
1654
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey);
1655
+ const reactions = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
1656
+ if (update.vote?.selectedOptions?.length) {
684
1657
  reactions.push(update);
685
1658
  }
686
1659
  msg.pollUpdates = reactions;
687
1660
  };
688
- exports.updateMessageWithPollUpdate = updateMessageWithPollUpdate;
1661
+ /** Update the message with a new event response */
1662
+ export const updateMessageWithEventResponse = (msg, update) => {
1663
+ const authorID = getKeyAuthor(update.eventResponseMessageKey);
1664
+ const responses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID);
1665
+ responses.push(update);
1666
+ msg.eventResponses = responses;
1667
+ };
689
1668
  /**
690
1669
  * Aggregates all poll updates in a poll.
691
1670
  * @param msg the poll creation message
692
1671
  * @param meId your jid
693
1672
  * @returns A list of options & their voters
694
1673
  */
695
- function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
696
- var _a, _b, _c;
697
- const opts = ((_a = message === null || message === void 0 ? void 0 : message.pollCreationMessage) === null || _a === void 0 ? void 0 : _a.options) || ((_b = message === null || message === void 0 ? void 0 : message.pollCreationMessageV2) === null || _b === void 0 ? void 0 : _b.options) || ((_c = message === null || message === void 0 ? void 0 : message.pollCreationMessageV3) === null || _c === void 0 ? void 0 : _c.options) || [];
1674
+ export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
1675
+ const opts = message?.pollCreationMessage?.options ||
1676
+ message?.pollCreationMessageV2?.options ||
1677
+ message?.pollCreationMessageV3?.options ||
1678
+ [];
698
1679
  const voteHashMap = opts.reduce((acc, opt) => {
699
- const hash = (0, crypto_2.sha256)(Buffer.from(opt.optionName || '')).toString();
1680
+ const hash = sha256(Buffer.from(opt.optionName || '')).toString();
700
1681
  acc[hash] = {
701
1682
  name: opt.optionName || '',
702
1683
  voters: []
@@ -718,14 +1699,36 @@ function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
718
1699
  };
719
1700
  data = voteHashMap[hash];
720
1701
  }
721
- voteHashMap[hash].voters.push((0, generics_1.getKeyAuthor)(update.pollUpdateMessageKey, meId));
1702
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
722
1703
  }
723
1704
  }
724
1705
  return Object.values(voteHashMap);
725
1706
  }
726
- exports.getAggregateVotesInPollMessage = getAggregateVotesInPollMessage;
1707
+ /**
1708
+ * Aggregates all event responses in an event message.
1709
+ * @param msg the event creation message
1710
+ * @param meId your jid
1711
+ * @returns A list of response types & their responders
1712
+ */
1713
+ export function getAggregateResponsesInEventMessage({ eventResponses }, meId) {
1714
+ const responseTypes = ['GOING', 'NOT_GOING', 'MAYBE'];
1715
+ const responseMap = {};
1716
+ for (const type of responseTypes) {
1717
+ responseMap[type] = {
1718
+ response: type,
1719
+ responders: []
1720
+ };
1721
+ }
1722
+ for (const update of eventResponses || []) {
1723
+ const responseType = update.eventResponse || 'UNKNOWN';
1724
+ if (responseType !== 'UNKNOWN' && responseMap[responseType]) {
1725
+ responseMap[responseType].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId));
1726
+ }
1727
+ }
1728
+ return Object.values(responseMap);
1729
+ }
727
1730
  /** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
728
- const aggregateMessageKeysNotFromMe = (keys) => {
1731
+ export const aggregateMessageKeysNotFromMe = (keys) => {
729
1732
  const keyMap = {};
730
1733
  for (const { remoteJid, id, participant, fromMe } of keys) {
731
1734
  if (!fromMe) {
@@ -742,40 +1745,34 @@ const aggregateMessageKeysNotFromMe = (keys) => {
742
1745
  }
743
1746
  return Object.values(keyMap);
744
1747
  };
745
- exports.aggregateMessageKeysNotFromMe = aggregateMessageKeysNotFromMe;
746
1748
  const REUPLOAD_REQUIRED_STATUS = [410, 404];
747
1749
  /**
748
1750
  * Downloads the given message. Throws an error if it's not a media message
749
1751
  */
750
- const downloadMediaMessage = async (message, type, options, ctx) => {
751
- const result = await downloadMsg()
752
- .catch(async (error) => {
753
- var _a;
754
- if (ctx) {
755
- if (axios_1.default.isAxiosError(error)) {
756
- // check if the message requires a reupload
757
- if (REUPLOAD_REQUIRED_STATUS.includes((_a = error.response) === null || _a === void 0 ? void 0 : _a.status)) {
758
- ctx.logger.info({ key: message.key }, 'sending reupload media request...');
759
- // request reupload
760
- message = await ctx.reuploadRequest(message);
761
- const result = await downloadMsg();
762
- return result;
763
- }
764
- }
1752
+ export const downloadMediaMessage = async (message, type, options, ctx) => {
1753
+ const result = await downloadMsg().catch(async (error) => {
1754
+ if (ctx &&
1755
+ typeof error?.status === 'number' && // treat errors with status as HTTP failures requiring reupload
1756
+ REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
1757
+ ctx.logger.info({ key: message.key }, 'sending reupload media request...');
1758
+ // request reupload
1759
+ message = await ctx.reuploadRequest(message);
1760
+ const result = await downloadMsg();
1761
+ return result;
765
1762
  }
766
1763
  throw error;
767
1764
  });
768
1765
  return result;
769
1766
  async function downloadMsg() {
770
- const mContent = (0, exports.extractMessageContent)(message.message);
1767
+ const mContent = extractMessageContent(message.message);
771
1768
  if (!mContent) {
772
- throw new boom_1.Boom('No message present', { statusCode: 400, data: message });
1769
+ throw new Boom('No message present', { statusCode: 400, data: message });
773
1770
  }
774
- const contentType = (0, exports.getContentType)(mContent);
775
- let mediaType = contentType === null || contentType === void 0 ? void 0 : contentType.replace('Message', '');
1771
+ const contentType = getContentType(mContent);
1772
+ let mediaType = contentType?.replace('Message', '');
776
1773
  const media = mContent[contentType];
777
1774
  if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
778
- throw new boom_1.Boom(`"${contentType}" message is not a media message`);
1775
+ throw new Boom(`"${contentType}" message is not a media message`);
779
1776
  }
780
1777
  let download;
781
1778
  if ('thumbnailDirectPath' in media && !('url' in media)) {
@@ -788,7 +1785,7 @@ const downloadMediaMessage = async (message, type, options, ctx) => {
788
1785
  else {
789
1786
  download = media;
790
1787
  }
791
- const stream = await (0, messages_media_1.downloadContentFromMessage)(download, mediaType, options);
1788
+ const stream = await downloadContentFromMessage(download, mediaType, options);
792
1789
  if (type === 'buffer') {
793
1790
  const bufferArray = [];
794
1791
  for await (const chunk of stream) {
@@ -799,21 +1796,90 @@ const downloadMediaMessage = async (message, type, options, ctx) => {
799
1796
  return stream;
800
1797
  }
801
1798
  };
802
- exports.downloadMediaMessage = downloadMediaMessage;
803
1799
  /** Checks whether the given message is a media message; if it is returns the inner content */
804
- const assertMediaContent = (content) => {
805
- content = (0, exports.extractMessageContent)(content);
806
- const mediaContent = (content === null || content === void 0 ? void 0 : content.documentMessage)
807
- || (content === null || content === void 0 ? void 0 : content.imageMessage)
808
- || (content === null || content === void 0 ? void 0 : content.videoMessage)
809
- || (content === null || content === void 0 ? void 0 : content.audioMessage)
810
- || (content === null || content === void 0 ? void 0 : content.stickerMessage);
1800
+ export const assertMediaContent = (content) => {
1801
+ content = extractMessageContent(content);
1802
+ const mediaContent = content?.documentMessage ||
1803
+ content?.imageMessage ||
1804
+ content?.videoMessage ||
1805
+ content?.audioMessage ||
1806
+ content?.stickerMessage;
811
1807
  if (!mediaContent) {
812
- throw new boom_1.Boom('given message is not a media message', {
813
- statusCode: 400,
814
- data: content
815
- });
1808
+ throw new Boom('given message is not a media message', { statusCode: 400, data: content });
816
1809
  }
817
1810
  return mediaContent;
818
1811
  };
819
- exports.assertMediaContent = assertMediaContent;
1812
+ /**
1813
+ * Checks if a WebP buffer is animated by looking for VP8X chunk with animation flag
1814
+ * or ANIM/ANMF chunks
1815
+ */
1816
+ const isAnimatedWebP = (buffer) => {
1817
+ // WebP must start with RIFF....WEBP
1818
+ if (
1819
+ buffer.length < 12 ||
1820
+ buffer[0] !== 0x52 ||
1821
+ buffer[1] !== 0x49 ||
1822
+ buffer[2] !== 0x46 ||
1823
+ buffer[3] !== 0x46 ||
1824
+ buffer[8] !== 0x57 ||
1825
+ buffer[9] !== 0x45 ||
1826
+ buffer[10] !== 0x42 ||
1827
+ buffer[11] !== 0x50
1828
+ ) {
1829
+ return false;
1830
+ };
1831
+ // Parse chunks starting after RIFF header (12 bytes)
1832
+ let offset = 12;
1833
+ while (offset < buffer.length - 8) {
1834
+ const chunkFourCC = buffer.toString('ascii', offset, offset + 4);
1835
+ const chunkSize = buffer.readUInt32LE(offset + 4);
1836
+ if (chunkFourCC === 'VP8X') {
1837
+ // VP8X extended header, check animation flag (bit 1 at offset+8)
1838
+ const flagsOffset = offset + 8;
1839
+ if (flagsOffset < buffer.length) {
1840
+ const flags = buffer[flagsOffset];
1841
+ if (flags & 0x02) {
1842
+ return true;
1843
+ };
1844
+ };
1845
+ } else if (chunkFourCC === 'ANIM' || chunkFourCC === 'ANMF') {
1846
+ // ANIM or ANMF chunks indicate animation
1847
+ return true;
1848
+ };
1849
+ // Move to next chunk (chunk size + 8 bytes header, padded to even)
1850
+ offset += 8 + chunkSize + (chunkSize % 2);
1851
+ };
1852
+ return false;
1853
+ };
1854
+ /**
1855
+ * Checks if a buffer is a WebP file
1856
+ */
1857
+ const isWebPBuffer = (buffer) => {
1858
+ return (
1859
+ buffer.length >= 12 &&
1860
+ buffer[0] === 0x52 &&
1861
+ buffer[1] === 0x49 &&
1862
+ buffer[2] === 0x46 &&
1863
+ buffer[3] === 0x46 &&
1864
+ buffer[8] === 0x57 &&
1865
+ buffer[9] === 0x45 &&
1866
+ buffer[10] === 0x42 &&
1867
+ buffer[11] === 0x50
1868
+ );
1869
+ };
1870
+ /**
1871
+ * vltcs@changes 30-01-26
1872
+ * ---
1873
+ * Determines whether a message should include a Biz Binary Node.
1874
+ * A Biz Binary Node is added only for interactive messages
1875
+ * such as buttons or other supported interactive types.
1876
+ */
1877
+ export const shouldIncludeBizBinaryNode = (message) => {
1878
+ const messageType = getContentType(message);
1879
+ return (
1880
+ messageType === 'buttonsMessage' ||
1881
+ messageType === 'interactiveMessage' ||
1882
+ messageType === 'listMessage' ||
1883
+ messageType === 'templateMessage'
1884
+ );
1885
+ };