xmd-baileys 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/WAProto/index.js +169661 -0
  4. package/engine-requirements.js +10 -0
  5. package/lib/Defaults/baileys-version.json +3 -0
  6. package/lib/Defaults/constants.js +74 -0
  7. package/lib/Defaults/index.d.ts +53 -0
  8. package/lib/Defaults/index.js +147 -0
  9. package/lib/Defaults/media.js +48 -0
  10. package/lib/Defaults/phonenumber-mcc.json +223 -0
  11. package/lib/Signal/Group/ciphertext-message.d.ts +9 -0
  12. package/lib/Signal/Group/ciphertext-message.js +15 -0
  13. package/lib/Signal/Group/group-session-builder.d.ts +14 -0
  14. package/lib/Signal/Group/group-session-builder.js +64 -0
  15. package/lib/Signal/Group/group_cipher.d.ts +17 -0
  16. package/lib/Signal/Group/group_cipher.js +96 -0
  17. package/lib/Signal/Group/index.d.ts +11 -0
  18. package/lib/Signal/Group/index.js +57 -0
  19. package/lib/Signal/Group/keyhelper.d.ts +10 -0
  20. package/lib/Signal/Group/keyhelper.js +55 -0
  21. package/lib/Signal/Group/queue-job.d.ts +1 -0
  22. package/lib/Signal/Group/queue-job.js +57 -0
  23. package/lib/Signal/Group/sender-chain-key.d.ts +13 -0
  24. package/lib/Signal/Group/sender-chain-key.js +34 -0
  25. package/lib/Signal/Group/sender-key-distribution-message.d.ts +16 -0
  26. package/lib/Signal/Group/sender-key-distribution-message.js +66 -0
  27. package/lib/Signal/Group/sender-key-message.d.ts +18 -0
  28. package/lib/Signal/Group/sender-key-message.js +69 -0
  29. package/lib/Signal/Group/sender-key-name.d.ts +17 -0
  30. package/lib/Signal/Group/sender-key-name.js +51 -0
  31. package/lib/Signal/Group/sender-key-record.d.ts +30 -0
  32. package/lib/Signal/Group/sender-key-record.js +53 -0
  33. package/lib/Signal/Group/sender-key-state.d.ts +38 -0
  34. package/lib/Signal/Group/sender-key-state.js +99 -0
  35. package/lib/Signal/Group/sender-message-key.d.ts +11 -0
  36. package/lib/Signal/Group/sender-message-key.js +29 -0
  37. package/lib/Signal/Group/tmp +1 -0
  38. package/lib/Signal/libsignal.d.ts +3 -0
  39. package/lib/Signal/libsignal.js +174 -0
  40. package/lib/Socket/Client/abstract-socket-client.d.ts +17 -0
  41. package/lib/Socket/Client/abstract-socket-client.js +13 -0
  42. package/lib/Socket/Client/index.d.ts +3 -0
  43. package/lib/Socket/Client/index.js +19 -0
  44. package/lib/Socket/Client/mobile-socket-client.d.ts +13 -0
  45. package/lib/Socket/Client/mobile-socket-client.js +65 -0
  46. package/lib/Socket/Client/tmp +1 -0
  47. package/lib/Socket/Client/web-socket-client.d.ts +12 -0
  48. package/lib/Socket/Client/web-socket-client.js +62 -0
  49. package/lib/Socket/business.d.ts +171 -0
  50. package/lib/Socket/business.js +260 -0
  51. package/lib/Socket/chats.d.ts +267 -0
  52. package/lib/Socket/chats.js +970 -0
  53. package/lib/Socket/groups.d.ts +115 -0
  54. package/lib/Socket/groups.js +317 -0
  55. package/lib/Socket/index.d.ts +173 -0
  56. package/lib/Socket/index.js +11 -0
  57. package/lib/Socket/luxu.d.ts +268 -0
  58. package/lib/Socket/luxu.js +591 -0
  59. package/lib/Socket/messages-recv.d.ts +161 -0
  60. package/lib/Socket/messages-recv.js +1110 -0
  61. package/lib/Socket/messages-send.d.ts +149 -0
  62. package/lib/Socket/messages-send.js +912 -0
  63. package/lib/Socket/newsletter.d.ts +134 -0
  64. package/lib/Socket/newsletter.js +315 -0
  65. package/lib/Socket/registration.d.ts +267 -0
  66. package/lib/Socket/registration.js +166 -0
  67. package/lib/Socket/socket.d.ts +43 -0
  68. package/lib/Socket/socket.js +665 -0
  69. package/lib/Socket/usync.d.ts +36 -0
  70. package/lib/Socket/usync.js +70 -0
  71. package/lib/Store/index.d.ts +3 -0
  72. package/lib/Store/index.js +10 -0
  73. package/lib/Store/make-cache-manager-store.d.ts +13 -0
  74. package/lib/Store/make-cache-manager-store.js +83 -0
  75. package/lib/Store/make-in-memory-store.d.ts +118 -0
  76. package/lib/Store/make-in-memory-store.js +427 -0
  77. package/lib/Store/make-ordered-dictionary.d.ts +13 -0
  78. package/lib/Store/make-ordered-dictionary.js +81 -0
  79. package/lib/Store/object-repository.d.ts +10 -0
  80. package/lib/Store/object-repository.js +27 -0
  81. package/lib/Store/tmp +1 -0
  82. package/lib/Types/Auth.d.ts +110 -0
  83. package/lib/Types/Auth.js +2 -0
  84. package/lib/Types/Call.d.ts +13 -0
  85. package/lib/Types/Call.js +2 -0
  86. package/lib/Types/Chat.d.ts +102 -0
  87. package/lib/Types/Chat.js +4 -0
  88. package/lib/Types/Contact.d.ts +19 -0
  89. package/lib/Types/Contact.js +2 -0
  90. package/lib/Types/Events.d.ts +157 -0
  91. package/lib/Types/Events.js +2 -0
  92. package/lib/Types/GroupMetadata.d.ts +55 -0
  93. package/lib/Types/GroupMetadata.js +2 -0
  94. package/lib/Types/Label.d.ts +35 -0
  95. package/lib/Types/Label.js +27 -0
  96. package/lib/Types/LabelAssociation.d.ts +29 -0
  97. package/lib/Types/LabelAssociation.js +9 -0
  98. package/lib/Types/Message.d.ts +273 -0
  99. package/lib/Types/Message.js +9 -0
  100. package/lib/Types/Newsletter.d.ts +103 -0
  101. package/lib/Types/Newsletter.js +38 -0
  102. package/lib/Types/Product.d.ts +78 -0
  103. package/lib/Types/Product.js +2 -0
  104. package/lib/Types/Signal.d.ts +57 -0
  105. package/lib/Types/Signal.js +2 -0
  106. package/lib/Types/Socket.d.ts +111 -0
  107. package/lib/Types/Socket.js +2 -0
  108. package/lib/Types/State.d.ts +27 -0
  109. package/lib/Types/State.js +2 -0
  110. package/lib/Types/USync.d.ts +25 -0
  111. package/lib/Types/USync.js +2 -0
  112. package/lib/Types/index.d.ts +57 -0
  113. package/lib/Types/index.js +42 -0
  114. package/lib/Types/tmp +1 -0
  115. package/lib/Utils/auth-utils.d.ts +18 -0
  116. package/lib/Utils/auth-utils.js +206 -0
  117. package/lib/Utils/baileys-event-stream.d.ts +16 -0
  118. package/lib/Utils/baileys-event-stream.js +63 -0
  119. package/lib/Utils/business.d.ts +22 -0
  120. package/lib/Utils/business.js +234 -0
  121. package/lib/Utils/chat-utils.d.ts +71 -0
  122. package/lib/Utils/chat-utils.js +729 -0
  123. package/lib/Utils/crypto.d.ts +41 -0
  124. package/lib/Utils/crypto.js +151 -0
  125. package/lib/Utils/decode-wa-message.d.ts +19 -0
  126. package/lib/Utils/decode-wa-message.js +198 -0
  127. package/lib/Utils/event-buffer.d.ts +35 -0
  128. package/lib/Utils/event-buffer.js +514 -0
  129. package/lib/Utils/generics.d.ts +92 -0
  130. package/lib/Utils/generics.js +423 -0
  131. package/lib/Utils/history.d.ts +15 -0
  132. package/lib/Utils/history.js +96 -0
  133. package/lib/Utils/index.d.ts +17 -0
  134. package/lib/Utils/index.js +33 -0
  135. package/lib/Utils/link-preview.d.ts +21 -0
  136. package/lib/Utils/link-preview.js +93 -0
  137. package/lib/Utils/logger.d.ts +4 -0
  138. package/lib/Utils/logger.js +7 -0
  139. package/lib/Utils/lt-hash.d.ts +12 -0
  140. package/lib/Utils/lt-hash.js +51 -0
  141. package/lib/Utils/make-mutex.d.ts +7 -0
  142. package/lib/Utils/make-mutex.js +43 -0
  143. package/lib/Utils/messages-media.d.ts +116 -0
  144. package/lib/Utils/messages-media.js +1109 -0
  145. package/lib/Utils/messages.d.ts +77 -0
  146. package/lib/Utils/messages.js +1858 -0
  147. package/lib/Utils/noise-handler.d.ts +21 -0
  148. package/lib/Utils/noise-handler.js +155 -0
  149. package/lib/Utils/process-message.d.ts +41 -0
  150. package/lib/Utils/process-message.js +321 -0
  151. package/lib/Utils/signal.d.ts +32 -0
  152. package/lib/Utils/signal.js +153 -0
  153. package/lib/Utils/tmp +1 -0
  154. package/lib/Utils/use-multi-file-auth-state.d.ts +13 -0
  155. package/lib/Utils/use-multi-file-auth-state.js +119 -0
  156. package/lib/Utils/validate-connection.d.ts +11 -0
  157. package/lib/Utils/validate-connection.js +229 -0
  158. package/lib/WABinary/constants.d.ts +30 -0
  159. package/lib/WABinary/constants.js +40 -0
  160. package/lib/WABinary/decode.d.ts +7 -0
  161. package/lib/WABinary/decode.js +252 -0
  162. package/lib/WABinary/encode.d.ts +3 -0
  163. package/lib/WABinary/encode.js +265 -0
  164. package/lib/WABinary/generic-utils.d.ts +17 -0
  165. package/lib/WABinary/generic-utils.js +198 -0
  166. package/lib/WABinary/index.d.ts +5 -0
  167. package/lib/WABinary/index.js +21 -0
  168. package/lib/WABinary/jid-utils.d.ts +31 -0
  169. package/lib/WABinary/jid-utils.js +65 -0
  170. package/lib/WABinary/tmp +1 -0
  171. package/lib/WABinary/types.d.ts +18 -0
  172. package/lib/WABinary/types.js +2 -0
  173. package/lib/WAM/BinaryInfo.d.ts +17 -0
  174. package/lib/WAM/BinaryInfo.js +13 -0
  175. package/lib/WAM/constants.d.ts +38 -0
  176. package/lib/WAM/constants.js +15350 -0
  177. package/lib/WAM/encode.d.ts +3 -0
  178. package/lib/WAM/encode.js +155 -0
  179. package/lib/WAM/index.d.ts +3 -0
  180. package/lib/WAM/index.js +19 -0
  181. package/lib/WAM/tmp +1 -0
  182. package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +9 -0
  183. package/lib/WAUSync/Protocols/USyncContactProtocol.js +32 -0
  184. package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +22 -0
  185. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +57 -0
  186. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +12 -0
  187. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +30 -0
  188. package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +12 -0
  189. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +42 -0
  190. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +25 -0
  191. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +53 -0
  192. package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +8 -0
  193. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +24 -0
  194. package/lib/WAUSync/Protocols/index.d.ts +4 -0
  195. package/lib/WAUSync/Protocols/index.js +20 -0
  196. package/lib/WAUSync/Protocols/tmp +1 -0
  197. package/lib/WAUSync/USyncQuery.d.ts +28 -0
  198. package/lib/WAUSync/USyncQuery.js +89 -0
  199. package/lib/WAUSync/USyncUser.d.ts +12 -0
  200. package/lib/WAUSync/USyncUser.js +26 -0
  201. package/lib/WAUSync/index.js +19 -0
  202. package/lib/index.d.ts +12 -0
  203. package/lib/index.js +39 -0
  204. package/lib/temp +1 -0
  205. package/package.json +109 -0
@@ -0,0 +1,1858 @@
1
+ "use strict"
2
+
3
+ Object.defineProperty(exports, "__esModule", { value: true })
4
+
5
+ const { Boom } = require("@hapi/boom")
6
+ const { randomBytes } = require("crypto")
7
+ const { promises } = require("fs")
8
+ const { proto } = require("../../WAProto")
9
+ const {
10
+ URL_REGEX,
11
+ WA_DEFAULT_EPHEMERAL
12
+ } = require("../Defaults/constants")
13
+ const { MEDIA_KEYS } = require("../Defaults/media")
14
+ const {
15
+ WAProto,
16
+ WAMessageStatus
17
+ } = require("../Types")
18
+ const {
19
+ isJidGroup,
20
+ isJidNewsletter,
21
+ isJidStatusBroadcast,
22
+ jidNormalizedUser
23
+ } = require("../WABinary")
24
+ const { sha256 } = require("./crypto")
25
+ const {
26
+ generateMessageID,
27
+ getKeyAuthor,
28
+ unixTimestampSeconds
29
+ } = require("./generics")
30
+ const {
31
+ downloadContentFromMessage,
32
+ encryptedStream,
33
+ generateThumbnail,
34
+ getAudioDuration,
35
+ getAudioWaveform,
36
+ getRawMediaUploadData,
37
+ getStream,
38
+ toBuffer
39
+ } = require("./messages-media")
40
+
41
+ const MIMETYPE_MAP = {
42
+ image: 'image/jpeg',
43
+ video: 'video/mp4',
44
+ document: 'application/pdf',
45
+ audio: 'audio/ogg codecs=opus',
46
+ sticker: 'image/webp',
47
+ 'product-catalog-image': 'image/jpeg'
48
+ }
49
+
50
+ const MessageTypeProto = {
51
+ 'image': WAProto.Message.ImageMessage,
52
+ 'video': WAProto.Message.VideoMessage,
53
+ 'audio': WAProto.Message.AudioMessage,
54
+ 'sticker': WAProto.Message.StickerMessage,
55
+ 'document': WAProto.Message.DocumentMessage
56
+ }
57
+
58
+ /**
59
+ * Uses a regex to test whether the string contains a URL, and returns the URL if it does.
60
+ * @param text eg. hello https://google.com
61
+ * @returns the URL, eg. https://google.com
62
+ */
63
+ const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0]
64
+
65
+ const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
66
+ const url = extractUrlFromText(text)
67
+
68
+ if (!!getUrlInfo && url) {
69
+ try {
70
+ const urlInfo = await getUrlInfo(url)
71
+ return urlInfo
72
+ }
73
+
74
+ catch (error) {
75
+ logger?.warn({ trace: error.stack }, 'url generation failed')
76
+ }
77
+ }
78
+ }
79
+
80
+ const assertColor = async (color) => {
81
+ let assertedColor
82
+
83
+ if (typeof color === 'number') {
84
+ assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1
85
+ }
86
+
87
+ else {
88
+ let hex = color.trim().replace('#', '')
89
+ if (hex.length <= 6) {
90
+ hex = 'FF' + hex.padStart(6, '0')
91
+ }
92
+ assertedColor = parseInt(hex, 16)
93
+ return assertedColor
94
+ }
95
+ }
96
+
97
+ const prepareWAMessageMedia = async (message, options) => {
98
+ const logger = options.logger
99
+
100
+ let mediaType
101
+
102
+ for (const key of MEDIA_KEYS) {
103
+ if (key in message) {
104
+ mediaType = key
105
+ }
106
+ }
107
+
108
+ if (!mediaType) {
109
+ throw new Boom('Invalid media type', { statusCode: 400 })
110
+ }
111
+
112
+ const uploadData = {
113
+ ...message,
114
+ ...(message.annotations ? {
115
+ annotations: message.annotations
116
+ } : {
117
+ annotations: [
118
+ {
119
+ polygonVertices: [
120
+ {
121
+ x: 60.71664810180664,
122
+ y: -36.39784622192383
123
+ },
124
+ {
125
+ x: -16.710189819335938,
126
+ y: 49.263675689697266
127
+ },
128
+ {
129
+ x: -56.585853576660156,
130
+ y: 37.85963439941406
131
+ },
132
+ {
133
+ x: 20.840980529785156,
134
+ y: -47.80188751220703
135
+ }
136
+ ],
137
+ newsletter: {
138
+ newsletterJid: "120363402220977044@newsletter",
139
+ serverMessageId: 0,
140
+ newsletterName: "Queen Jusmy Md",
141
+ contentType: "UPDATE",
142
+ }
143
+ }
144
+ ]
145
+ }),
146
+ media: message[mediaType]
147
+ };
148
+
149
+
150
+ delete uploadData[mediaType]
151
+
152
+ // check if cacheable + generate cache key
153
+ const cacheableKey = typeof uploadData.media === 'object' &&
154
+ 'url' in uploadData.media &&
155
+ !!uploadData.media.url &&
156
+ !!options.mediaCache &&
157
+ mediaType + ':' + uploadData.media.url.toString()
158
+
159
+ if (mediaType === 'document' && !uploadData.fileName) {
160
+ uploadData.fileName = 'file'
161
+ }
162
+
163
+ if (!uploadData.mimetype) {
164
+ uploadData.mimetype = MIMETYPE_MAP[mediaType]
165
+ }
166
+
167
+ if (cacheableKey) {
168
+ const mediaBuff = await options.mediaCache.get(cacheableKey)
169
+
170
+ if (mediaBuff) {
171
+ logger?.debug({ cacheableKey }, 'got media cache hit')
172
+
173
+ const obj = proto.Message.decode(mediaBuff)
174
+ const key = `${mediaType}Message`
175
+
176
+ Object.assign(obj[key], { ...uploadData, media: undefined })
177
+
178
+ return obj
179
+ }
180
+ }
181
+
182
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid)
183
+
184
+ if (isNewsletter) {
185
+ logger?.info({ key: cacheableKey }, 'Preparing raw media for newsletter')
186
+ const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(uploadData.media, options.mediaTypeOverride || mediaType, logger)
187
+ const fileSha256B64 = fileSha256.toString('base64');
188
+ const { mediaUrl, directPath } = await options.upload(filePath, {
189
+ fileEncSha256B64: fileSha256B64,
190
+ mediaType: mediaType,
191
+ timeoutMs: options.mediaUploadTimeoutMs
192
+ })
193
+
194
+ await promises.unlink(filePath)
195
+
196
+ const obj = WAProto.Message.fromObject({
197
+ // todo: add more support here
198
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
199
+ url: mediaUrl,
200
+ directPath,
201
+ fileSha256,
202
+ fileLength,
203
+ ...uploadData,
204
+ media: undefined
205
+ })
206
+ })
207
+
208
+ if (uploadData.ptv) {
209
+ obj.ptvMessage = obj.videoMessage
210
+ delete obj.videoMessage
211
+ }
212
+
213
+ if (obj.stickerMessage) {
214
+ obj.stickerMessage.stickerSentTs = Date.now()
215
+ }
216
+
217
+ if (cacheableKey) {
218
+ logger?.debug({ cacheableKey }, 'set cache');
219
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish())
220
+ }
221
+
222
+ return obj
223
+ }
224
+
225
+ const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
226
+ const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'
227
+ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true
228
+ const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true
229
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
230
+ const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
231
+ logger,
232
+ saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
233
+ opts: options.options
234
+ })
235
+ const fileEncSha256B64 = fileEncSha256.toString('base64')
236
+ const [{ mediaUrl, directPath }] = await Promise.all([
237
+ (async () => {
238
+ const result = await options.upload(encFilePath, {
239
+ fileEncSha256B64,
240
+ mediaType,
241
+ timeoutMs: options.mediaUploadTimeoutMs
242
+ })
243
+ logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
244
+ return result
245
+ })(),
246
+ (async () => {
247
+ try {
248
+ if (requiresThumbnailComputation) {
249
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options)
250
+
251
+ uploadData.jpegThumbnail = thumbnail
252
+
253
+ if (!uploadData.width && originalImageDimensions) {
254
+ uploadData.width = originalImageDimensions.width
255
+ uploadData.height = originalImageDimensions.height
256
+ logger?.debug('set dimensions')
257
+ }
258
+ logger?.debug('generated thumbnail')
259
+ }
260
+
261
+ if (requiresDurationComputation) {
262
+ uploadData.seconds = await getAudioDuration(originalFilePath)
263
+ logger?.debug('computed audio duration')
264
+ }
265
+
266
+ if (requiresWaveformProcessing) {
267
+ uploadData.waveform = await getAudioWaveform(originalFilePath, logger)
268
+ logger?.debug('processed waveform')
269
+ }
270
+
271
+ if (requiresAudioBackground) {
272
+ uploadData.backgroundArgb = await assertColor(options.backgroundColor)
273
+ logger?.debug('computed backgroundColor audio status')
274
+ }
275
+ }
276
+ catch (error) {
277
+ logger?.warn({ trace: error.stack }, 'failed to obtain extra info')
278
+ }
279
+ })()
280
+ ]).finally(async () => {
281
+ try {
282
+ await promises.unlink(encFilePath)
283
+
284
+ if (originalFilePath) {
285
+ await promises.unlink(originalFilePath)
286
+ }
287
+
288
+ logger?.debug('removed tmp files')
289
+ }
290
+ catch (error) {
291
+ logger?.warn('failed to remove tmp file')
292
+ }
293
+ })
294
+
295
+ const obj = WAProto.Message.fromObject({
296
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
297
+ url: mediaUrl,
298
+ directPath,
299
+ mediaKey,
300
+ fileEncSha256,
301
+ fileSha256,
302
+ fileLength,
303
+ mediaKeyTimestamp: unixTimestampSeconds(),
304
+ ...uploadData,
305
+ media: undefined
306
+ })
307
+ })
308
+
309
+ if (uploadData.ptv) {
310
+ obj.ptvMessage = obj.videoMessage
311
+ delete obj.videoMessage
312
+ }
313
+
314
+ if (cacheableKey) {
315
+ logger?.debug({ cacheableKey }, 'set cache')
316
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish())
317
+ }
318
+
319
+ return obj
320
+ }
321
+
322
+ const prepareAlbumMessageContent = async (jid, albums, options) => {
323
+ if (!Array.isArray(albums)) {
324
+ throw new Error("albums must be an array containing media objects.")
325
+ }
326
+
327
+ if (albums.length === 0) {
328
+ throw new Error("albums cannot be empty. At least one media item is required.")
329
+ }
330
+
331
+ const validCount = albums.filter(m => ('image' in m) || ('video' in m)).length
332
+
333
+ if (validCount === 0) {
334
+ throw new Error("albums contains no valid media. Use 'image' or 'video' keys.")
335
+ }
336
+
337
+ let mediaHandle
338
+ let mediaMsg
339
+ const message = []
340
+
341
+ const albumMsg = generateWAMessageFromContent(jid, {
342
+ albumMessage: {
343
+ expectedImageCount: albums.filter(item => 'image' in item).length,
344
+ expectedVideoCount: albums.filter(item => 'video' in item).length
345
+ }
346
+ }, options)
347
+
348
+ await options.suki.relayMessage(jid, albumMsg.message, {
349
+ messageId: albumMsg.key.id
350
+ })
351
+
352
+ for (const media of albums) {
353
+ let content = {}
354
+ if ('image' in media) {
355
+ content = { image: media.image }
356
+ } else if ('video' in media) {
357
+ content = { video: media.video }
358
+ } else {
359
+ continue
360
+ }
361
+
362
+ mediaMsg = await generateWAMessage(
363
+ jid,
364
+ {
365
+ ...content,
366
+ ...media
367
+ },
368
+ {
369
+ userJid: options.userJid,
370
+ upload: async (encFilePath, opts) => {
371
+ const up = await options.suki.waUploadToServer(
372
+ encFilePath,
373
+ { ...opts, newsletter: isJidNewsletter(jid) }
374
+ )
375
+ mediaHandle = up.handle
376
+ return up
377
+ },
378
+ ...options
379
+ }
380
+ )
381
+
382
+ if (mediaMsg) {
383
+ mediaMsg.message.messageContextInfo = {
384
+ messageSecret: randomBytes(32),
385
+ messageAssociation: {
386
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM,
387
+ parentMessageKey: albumMsg.key
388
+ }
389
+ }
390
+ }
391
+
392
+ message.push(mediaMsg)
393
+ }
394
+
395
+ return message
396
+ }
397
+
398
+ const prepareDisappearingMessageSettingContent = (expiration) => {
399
+ const content = {
400
+ ephemeralMessage: {
401
+ message: {
402
+ protocolMessage: {
403
+ type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
404
+ ephemeralExpiration: expiration ? expiration : 0
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ return WAProto.Message.fromObject(content)
411
+ }
412
+
413
+ /**
414
+ * Generate forwarded message content like WA does
415
+ * @param message the message to forward
416
+ * @param options.forceForward will show the message as forwarded even if it is from you
417
+ */
418
+ const generateForwardMessageContent = (message, forceForward) => {
419
+ let content = message.message
420
+
421
+ if (!content) {
422
+ throw new Boom('no content in message', { statusCode: 400 })
423
+ }
424
+
425
+ // hacky copy
426
+ content = normalizeMessageContent(content)
427
+ content = proto.Message.decode(proto.Message.encode(content).finish())
428
+
429
+ let key = Object.keys(content)[0]
430
+ let score = content[key].contextInfo?.forwardingScore || 0
431
+
432
+ if (forceForward) score += forceForward ? forceForward : 1
433
+
434
+ if (key === 'conversation') {
435
+ content.extendedTextMessage = { text: content[key] }
436
+ delete content.conversation
437
+ key = 'extendedTextMessage'
438
+ }
439
+
440
+ if (score > 0) {
441
+ content[key].contextInfo = { forwardingScore: score, isForwarded: true }
442
+ }
443
+
444
+ else {
445
+ content[key].contextInfo = {}
446
+ }
447
+
448
+ return content
449
+ }
450
+
451
+ const hasNonNullishProperty = (message, key) => {
452
+ return (
453
+ typeof message === 'object' &&
454
+ message !== null &&
455
+ key in message &&
456
+ message[key] !== null &&
457
+ message[key] !== undefined
458
+ )
459
+ }
460
+
461
+ function hasOptionalProperty(obj, key) {
462
+ return (
463
+ typeof obj === 'object' &&
464
+ obj !== null &&
465
+ key in obj &&
466
+ obj[key] !== null
467
+ )
468
+ }
469
+
470
+ const generateWAMessageContent = async (message, options) => {
471
+ let m = {}
472
+
473
+ if (hasNonNullishProperty(message, "text")) {
474
+ const extContent = { text: message.text }
475
+
476
+ let urlInfo = message.linkPreview
477
+
478
+ if (typeof urlInfo === "undefined") {
479
+ urlInfo = await generateLinkPreviewIfRequired(
480
+ message.text,
481
+ options.getUrlInfo,
482
+ options.logger
483
+ )
484
+ }
485
+
486
+ if (urlInfo) {
487
+ extContent.canonicalUrl = urlInfo["canonical-url"]
488
+ extContent.matchedText = urlInfo["matched-text"]
489
+ extContent.jpegThumbnail = urlInfo.jpegThumbnail
490
+ extContent.description = urlInfo.description
491
+ extContent.title = urlInfo.title
492
+ extContent.previewType = 0
493
+
494
+ const img = urlInfo.highQualityThumbnail
495
+
496
+ if (img) {
497
+ extContent.thumbnailDirectPath = img.directPath
498
+ extContent.mediaKey = img.mediaKey
499
+ extContent.mediaKeyTimestamp = img.mediaKeyTimestamp
500
+ extContent.thumbnailWidth = img.width
501
+ extContent.thumbnailHeight = img.height
502
+ extContent.thumbnailSha256 = img.fileSha256
503
+ extContent.thumbnailEncSha256 = img.fileEncSha256
504
+ }
505
+ }
506
+
507
+ if (options.backgroundColor) {
508
+ extContent.backgroundArgb = await assertColor(
509
+ options.backgroundColor
510
+ )
511
+ }
512
+
513
+ if (options.textColor) {
514
+ extContent.textArgb = await assertColor(options.textColor)
515
+ }
516
+
517
+ if (options.font) {
518
+ extContent.font = options.font
519
+ }
520
+
521
+ m.extendedTextMessage = extContent
522
+ } else if (hasNonNullishProperty(message, "contacts")) {
523
+ const contactLen = message.contacts.contacts.length
524
+
525
+ let contactMessage
526
+
527
+ if (!contactLen) {
528
+ throw new Boom("require atleast 1 contact", { statusCode: 400 })
529
+ }
530
+
531
+ if (contactLen === 1) {
532
+ contactMessage = {
533
+ contactMessage: WAProto.Message.ContactMessage.fromObject(
534
+ message.contacts.contacts[0]
535
+ ),
536
+ }
537
+ } else {
538
+ contactMessage = {
539
+ contactsArrayMessage: WAProto.Message.ContactsArrayMessage.fromObject(
540
+ message.contacts
541
+ ),
542
+ }
543
+ }
544
+
545
+ m = contactMessage
546
+ } else if (hasNonNullishProperty(message, "contacts")) {
547
+ const contactLen = message.contacts.contacts.length
548
+
549
+ let contactMessage
550
+
551
+ if (!contactLen) {
552
+ throw new Boom("require atleast 1 contact", { statusCode: 400 })
553
+ }
554
+
555
+ if (contactLen === 1) {
556
+ contactMessage = {
557
+ contactMessage: WAProto.Message.ContactMessage.fromObject(
558
+ message.contacts.contacts[0]
559
+ ),
560
+ }
561
+ } else {
562
+ contactMessage = {
563
+ contactsArrayMessage: WAProto.Message.ContactsArrayMessage.fromObject(
564
+ message.contacts
565
+ ),
566
+ }
567
+ }
568
+
569
+ m = contactMessage
570
+ } else if (hasNonNullishProperty(message, "location")) {
571
+ let locationMessage
572
+
573
+ if (message.live) {
574
+ locationMessage = {
575
+ liveLocationMessage: WAProto.Message.LiveLocationMessage.fromObject(
576
+ message.location
577
+ ),
578
+ }
579
+ } else {
580
+ locationMessage = {
581
+ locationMessage: WAProto.Message.LocationMessage.fromObject(
582
+ message.location
583
+ ),
584
+ }
585
+ }
586
+
587
+ m = locationMessage
588
+ } else if (hasNonNullishProperty(message, "react")) {
589
+ if (!message.react.senderTimestampMs) {
590
+ message.react.senderTimestampMs = Date.now()
591
+ }
592
+
593
+ m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(
594
+ message.react
595
+ )
596
+ } else if (hasNonNullishProperty(message, "delete")) {
597
+ m.protocolMessage = {
598
+ key: message.delete,
599
+ type: WAProto.Message.ProtocolMessage.Type.REVOKE,
600
+ }
601
+ } else if (hasNonNullishProperty(message, "sharePhoneNumber")) {
602
+ m.protocolMessage = {
603
+ type: WAProto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER,
604
+ }
605
+ } else if (hasNonNullishProperty(message, "requestPhoneNumber")) {
606
+ m.requestPhoneNumberMessage = {}
607
+ } else if (hasNonNullishProperty(message, "forward")) {
608
+ m = generateForwardMessageContent(message.forward, message.force)
609
+ } else if (hasNonNullishProperty(message, "disappearingMessagesInChat")) {
610
+ const exp =
611
+ typeof message.disappearingMessagesInChat === "boolean"
612
+ ? message.disappearingMessagesInChat
613
+ ? WA_DEFAULT_EPHEMERAL
614
+ : 0
615
+ : message.disappearingMessagesInChat
616
+ m = prepareDisappearingMessageSettingContent(exp)
617
+ } else if (hasNonNullishProperty(message, "groupInvite")) {
618
+ m.groupInviteMessage = {}
619
+
620
+ m.groupInviteMessage.inviteCode = message.groupInvite.code
621
+ m.groupInviteMessage.inviteExpiration = message.groupInvite.expiration
622
+ m.groupInviteMessage.caption = message.groupInvite.caption
623
+ m.groupInviteMessage.groupJid = message.groupInvite.jid
624
+ m.groupInviteMessage.groupName = message.groupInvite.name
625
+ m.groupInviteMessage.contextInfo = message.contextInfo
626
+
627
+ if (options.getProfilePicUrl) {
628
+ const pfpUrl = await options.getProfilePicUrl(
629
+ message.groupInvite.jid
630
+ )
631
+ const { thumbnail } = await generateThumbnail(pfpUrl, "image")
632
+ m.groupInviteMessage.jpegThumbnail = thumbnail
633
+ }
634
+ } else if (hasNonNullishProperty(message, "adminInvite")) {
635
+ m.newsletterAdminInviteMessage = {}
636
+
637
+ m.newsletterAdminInviteMessage.newsletterJid = message.adminInvite.jid
638
+ m.newsletterAdminInviteMessage.newsletterName =
639
+ message.adminInvite.name
640
+ m.newsletterAdminInviteMessage.caption = message.adminInvite.caption
641
+ m.newsletterAdminInviteMessage.inviteExpiration =
642
+ message.adminInvite.expiration
643
+ m.newsletterAdminInviteMessage.contextInfo = message.contextInfo
644
+
645
+ if (options.getProfilePicUrl) {
646
+ const pfpUrl = await options.getProfilePicUrl(
647
+ message.adminInvite.jid
648
+ )
649
+ const { thumbnail } = await generateThumbnail(pfpUrl, "image")
650
+ m.newsletterAdminInviteMessage.jpegThumbnail = thumbnail
651
+ }
652
+ } else if (hasNonNullishProperty(message, "keep")) {
653
+ m.keepInChatMessage = {}
654
+
655
+ m.keepInChatMessage.key = message.keep.key
656
+ m.keepInChatMessage.keepType = message.keep?.type || 1
657
+ m.keepInChatMessage.timestampMs = message.keep?.time || Date.now()
658
+ } else if (hasNonNullishProperty(message, "call")) {
659
+ m.scheduledCallCreationMessage = {}
660
+
661
+ m.scheduledCallCreationMessage.scheduledTimestampMs =
662
+ message.call?.time || Date.now()
663
+ m.scheduledCallCreationMessage.callType = message.call?.type || 1
664
+ m.scheduledCallCreationMessage.title =
665
+ message.call?.name || "Call Creation"
666
+ } else if (hasNonNullishProperty(message, "paymentInvite")) {
667
+ m.messageContextInfo = {}
668
+ m.paymentInviteMessage = {}
669
+
670
+ m.paymentInviteMessage.expiryTimestamp =
671
+ message.paymentInvite?.expiry || 0
672
+ m.paymentInviteMessage.serviceType = message.paymentInvite?.type || 2
673
+ } else if (hasNonNullishProperty(message, "ptv")) {
674
+ const { videoMessage } = await prepareWAMessageMedia(
675
+ { video: message.video },
676
+ options
677
+ )
678
+
679
+ m.ptvMessage = videoMessage
680
+ } else if (hasNonNullishProperty(message, "order")) {
681
+ m.orderMessage = WAProto.Message.OrderMessage.fromObject(message.order)
682
+ } else if (hasNonNullishProperty(message, "product")) {
683
+ const { imageMessage } = await prepareWAMessageMedia(
684
+ { image: message.product.productImage },
685
+ options
686
+ )
687
+
688
+ m.productMessage = WAProto.Message.ProductMessage.fromObject({
689
+ ...message,
690
+ product: {
691
+ ...message.product,
692
+ productImage: imageMessage,
693
+ },
694
+ })
695
+ } else if (hasNonNullishProperty(message, "album")) {
696
+ const imageMessages = message.album.filter((item) => "image" in item)
697
+ const videoMessages = message.album.filter((item) => "video" in item)
698
+
699
+ m.albumMessage = WAProto.Message.AlbumMessage.fromObject({
700
+ expectedImageCount: imageMessages.length,
701
+ expectedVideoCount: videoMessages.length,
702
+ })
703
+ } else if (hasNonNullishProperty(message, "event")) {
704
+ m.eventMessage = WAProto.Message.EventMessage.fromObject(message.event)
705
+
706
+ if (!message.event.startTime) {
707
+ m.eventMessage.startTime = unixTimestampSeconds() + 86400
708
+ }
709
+
710
+ if (options.getCallLink && message.event.call) {
711
+ const link = await options.getCallLink(
712
+ message.event.call,
713
+ m.eventMessage.startTime
714
+ )
715
+ m.eventMessage.joinLink = link
716
+ }
717
+ } else if (hasNonNullishProperty(message, "pollResult")) {
718
+ if (!Array.isArray(message.pollResult.values)) {
719
+ throw new Boom("Invalid pollResult values", { statusCode: 400 })
720
+ }
721
+
722
+ const pollResultSnapshotMessage = {
723
+ name: message.pollResult.name,
724
+ pollVotes: message.pollResult.values.map(
725
+ ([optionName, optionVoteCount]) => ({
726
+ optionName,
727
+ optionVoteCount,
728
+ })
729
+ ),
730
+ }
731
+
732
+ m.pollResultSnapshotMessage = pollResultSnapshotMessage
733
+ } else if (hasNonNullishProperty(message, "poll")) {
734
+ if (!Array.isArray(message.poll.values)) {
735
+ throw new Boom("Invalid poll values", { statusCode: 400 })
736
+ }
737
+
738
+ if (
739
+ message.poll.selectableCount < 0 ||
740
+ message.poll.selectableCount > message.poll.values.length
741
+ ) {
742
+ throw new Boom(
743
+ `poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`,
744
+ { statusCode: 400 }
745
+ )
746
+ }
747
+
748
+ const pollCreationMessage = {
749
+ name: message.poll.name,
750
+ selectableOptionsCount: message.poll?.selectableCount || 0,
751
+ options: message.poll.values.map((optionName) => ({ optionName })),
752
+ }
753
+
754
+ if (message.poll?.toAnnouncementGroup) {
755
+ m.pollCreationMessageV2 = pollCreationMessage
756
+ } else {
757
+ if (message.poll.selectableCount > 0) {
758
+ m.pollCreationMessageV3 = pollCreationMessage
759
+ } else {
760
+ m.pollCreationMessage = pollCreationMessage
761
+ }
762
+ }
763
+ } else if (hasNonNullishProperty(message, "payment")) {
764
+ const requestPaymentMessage = {
765
+ amount: {
766
+ currencyCode: message.payment?.currency || "IDR",
767
+ offset: message.payment?.offset || 0,
768
+ value: message.payment?.amount || 999999999,
769
+ },
770
+ expiryTimestamp: message.payment?.expiry || 0,
771
+ amount1000: message.payment?.amount || 999999999 * 1000,
772
+ currencyCodeIso4217: message.payment?.currency || "IDR",
773
+ requestFrom: message.payment?.from || "0@s.whatsapp.net",
774
+ noteMessage: {
775
+ extendedTextMessage: {
776
+ text: message.payment?.note || "Notes",
777
+ },
778
+ },
779
+ background: {
780
+ placeholderArgb:
781
+ message.payment?.image?.placeholderArgb || 4278190080,
782
+ textArgb: message.payment?.image?.textArgb || 4294967295,
783
+ subtextArgb: message.payment?.image?.subtextArgb || 4294967295,
784
+ type: 1,
785
+ },
786
+ }
787
+
788
+ m.requestPaymentMessage = requestPaymentMessage
789
+ } else if (hasNonNullishProperty(message, "stickerPack")) {
790
+ const {
791
+ stickers,
792
+ cover,
793
+ name,
794
+ publisher,
795
+ packId,
796
+ description,
797
+ } = message.stickerPack
798
+
799
+ const { zip } = require("fflate")
800
+
801
+ const stickerData = {}
802
+ const stickerPromises = stickers.map(async (s, i) => {
803
+ const { stream } = await getStream(s.sticker)
804
+ const buffer = await toBuffer(stream)
805
+ const hash = sha256(buffer).toString("base64url")
806
+ const fileName = `${i.toString().padStart(2, "0")}_${hash}.webp`
807
+
808
+ stickerData[fileName] = [new Uint8Array(buffer), { level: 0 }]
809
+
810
+ return {
811
+ fileName,
812
+ mimetype: "image/webp",
813
+ isAnimated: s.isAnimated || false,
814
+ isLottie: s.isLottie || false,
815
+ emojis: s.emojis || [],
816
+ accessibilityLabel: s.accessibilityLabel || "",
817
+ }
818
+ })
819
+
820
+ const stickerMetadata = await Promise.all(stickerPromises)
821
+
822
+ const zipBuffer = await new Promise((resolve, reject) => {
823
+ zip(stickerData, (err, data) => {
824
+ if (err) {
825
+ reject(err)
826
+ } else {
827
+ resolve(Buffer.from(data))
828
+ }
829
+ })
830
+ })
831
+
832
+ const coverBuffer = await toBuffer((await getStream(cover)).stream)
833
+
834
+ const [stickerPackUpload, coverUpload] = await Promise.all([
835
+ encryptedStream(zipBuffer, "sticker-pack", {
836
+ logger: options.logger,
837
+ opts: options.options,
838
+ }),
839
+ prepareWAMessageMedia(
840
+ { image: coverBuffer },
841
+ { ...options, mediaTypeOverride: "image" }
842
+ ),
843
+ ])
844
+
845
+ const stickerPackUploadResult = await options.upload(
846
+ stickerPackUpload.encFilePath,
847
+ {
848
+ fileEncSha256B64: stickerPackUpload.fileEncSha256.toString(
849
+ "base64"
850
+ ),
851
+ mediaType: "sticker-pack",
852
+ timeoutMs: options.mediaUploadTimeoutMs,
853
+ }
854
+ )
855
+
856
+ const coverImage = coverUpload.imageMessage
857
+ const imageDataHash = sha256(coverBuffer).toString("base64")
858
+ const stickerPackId = packId || generateMessageID()
859
+
860
+ m.stickerPackMessage = {
861
+ name,
862
+ publisher,
863
+ stickerPackId,
864
+ packDescription: description,
865
+ stickerPackOrigin:
866
+ proto.Message.StickerPackMessage.StickerPackOrigin.THIRD_PARTY,
867
+ stickerPackSize: stickerPackUpload.fileLength,
868
+ stickers: stickerMetadata,
869
+ fileSha256: stickerPackUpload.fileSha256,
870
+ fileEncSha256: stickerPackUpload.fileEncSha256,
871
+ mediaKey: stickerPackUpload.mediaKey,
872
+ directPath: stickerPackUploadResult.directPath,
873
+ fileLength: stickerPackUpload.fileLength,
874
+ mediaKeyTimestamp: unixTimestampSeconds(),
875
+ trayIconFileName: `${stickerPackId}.png`,
876
+ imageDataHash,
877
+ thumbnailDirectPath: coverImage.directPath,
878
+ thumbnailFileSha256: coverImage.fileSha256,
879
+ thumbnailFileEncSha256: coverImage.fileEncSha256,
880
+ thumbnailHeight: coverImage.height,
881
+ thumbnailWidth: coverImage.width,
882
+ }
883
+ } else {
884
+ m = await prepareWAMessageMedia(message, options)
885
+ }
886
+
887
+ if (hasNonNullishProperty(message, "buttonReply")) {
888
+ switch (message.type) {
889
+ case "list":
890
+ m.listResponseMessage = {
891
+ title: message.buttonReply.title,
892
+ description: message.buttonReply.description,
893
+ singleSelectReply: {
894
+ selectedRowId: message.buttonReply.rowId,
895
+ },
896
+ lisType:
897
+ proto.Message.ListResponseMessage.ListType
898
+ .SINGLE_SELECT,
899
+ }
900
+ break
901
+ case "template":
902
+ m.templateButtonReplyMessage = {
903
+ selectedDisplayText: message.buttonReply.displayText,
904
+ selectedId: message.buttonReply.id,
905
+ selectedIndex: message.buttonReply.index,
906
+ }
907
+ break
908
+ case "plain":
909
+ m.buttonsResponseMessage = {
910
+ selectedButtonId: message.buttonReply.id,
911
+ selectedDisplayText: message.buttonReply.displayText,
912
+ type:
913
+ proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT,
914
+ }
915
+ break
916
+ case "interactive":
917
+ m.interactiveResponseMessage = {
918
+ body: {
919
+ text: message.buttonReply.displayText,
920
+ format:
921
+ proto.Message.InteractiveResponseMessage.Body.Format
922
+ .EXTENSIONS_1,
923
+ },
924
+ nativeFlowResponseMessage: {
925
+ name: message.buttonReply.nativeFlows.name,
926
+ paramsJson: message.buttonReply.nativeFlows.paramsJson,
927
+ version: message.buttonReply.nativeFlows.version,
928
+ },
929
+ }
930
+ break
931
+ }
932
+ } else if (hasNonNullishProperty(message, "sections")) {
933
+ m.listMessage = {
934
+ title: message.title,
935
+ buttonText: message.buttonText,
936
+ footerText: message.footer,
937
+ description: message.text,
938
+ sections: message.sections,
939
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
940
+ }
941
+ } else if (hasNonNullishProperty(message, "productList")) {
942
+ const thumbnail = message.thumbnail
943
+ ? await generateThumbnail(message.thumbnail, "image")
944
+ : null
945
+
946
+ m.listMessage = {
947
+ title: message.title,
948
+ buttonText: message.buttonText,
949
+ footerText: message.footer,
950
+ description: message.text,
951
+ productListInfo: {
952
+ productSections: message.productList,
953
+ headerImage: {
954
+ productId: message.productList[0].products[0].productId,
955
+ jpegThumbnail: thumbnail?.thumbnail || null,
956
+ },
957
+ businessOwnerJid: message.businessOwnerJid,
958
+ },
959
+ listType: proto.Message.ListMessage.ListType.PRODUCT_LIST,
960
+ }
961
+ } else if (hasNonNullishProperty(message, "buttons")) {
962
+ const buttonsMessage = {
963
+ buttons: message.buttons.map((b) => ({
964
+ ...b,
965
+ type: proto.Message.ButtonsMessage.Button.Type.RESPONSE,
966
+ })),
967
+ }
968
+
969
+ if (hasNonNullishProperty(message, "text")) {
970
+ buttonsMessage.contentText = message.text
971
+ buttonsMessage.headerType =
972
+ proto.Message.ButtonsMessage.HeaderType.EMPTY
973
+ } else {
974
+ if (hasNonNullishProperty(message, "caption")) {
975
+ buttonsMessage.contentText = message.caption
976
+ }
977
+
978
+ const type = Object.keys(m)[0].replace("Message", "").toUpperCase()
979
+
980
+ buttonsMessage.headerType =
981
+ proto.Message.ButtonsMessage.HeaderType[type]
982
+
983
+ Object.assign(buttonsMessage, m)
984
+ }
985
+
986
+ if (hasNonNullishProperty(message, "title")) {
987
+ buttonsMessage.text = message.title
988
+ buttonsMessage.headerType =
989
+ proto.Message.ButtonsMessage.HeaderType.TEXT
990
+ }
991
+
992
+ if (hasNonNullishProperty(message, "footer")) {
993
+ buttonsMessage.footerText = message.footer
994
+ }
995
+
996
+ m = { buttonsMessage }
997
+ } else if (hasNonNullishProperty(message, "templateButtons")) {
998
+ const hydratedTemplate = {
999
+ hydratedButtons: message.templateButtons,
1000
+ }
1001
+
1002
+ if (hasNonNullishProperty(message, "text")) {
1003
+ hydratedTemplate.hydratedContentText = message.text
1004
+ } else {
1005
+ if (hasNonNullishProperty(message, "caption")) {
1006
+ hydratedTemplate.hydratedContentText = message.caption
1007
+ }
1008
+
1009
+ Object.assign(msg, m)
1010
+ }
1011
+
1012
+ if (hasNonNullishProperty(message, "footer")) {
1013
+ hydratedTemplate.hydratedFooterText = message.footer
1014
+ }
1015
+
1016
+ m = { templateMessage: { hydratedTemplate } }
1017
+ } else if (hasNonNullishProperty(message, "interactiveButtons")) {
1018
+ const interactiveMessage = {
1019
+ nativeFlowMessage: {
1020
+ buttons: message.interactiveButtons,
1021
+ },
1022
+ }
1023
+
1024
+ if (hasNonNullishProperty(message, "text")) {
1025
+ interactiveMessage.body = {
1026
+ text: message.text,
1027
+ }
1028
+ }
1029
+
1030
+ if (hasNonNullishProperty(message, "title")) {
1031
+ interactiveMessage.header = {
1032
+ title: message.title,
1033
+ subtitle: null,
1034
+ hasMediaAttachment: false,
1035
+ }
1036
+
1037
+ if (hasNonNullishProperty(message, "subtitle")) {
1038
+ interactiveMessage.header.subtitle = message.subtitle
1039
+ }
1040
+ } else {
1041
+ if (hasNonNullishProperty(message, "caption")) {
1042
+ interactiveMessage.body = {
1043
+ text: message.caption,
1044
+ }
1045
+
1046
+ interactiveMessage.header = {
1047
+ title: null,
1048
+ subtitle: null,
1049
+ hasMediaAttachment: false,
1050
+ ...Object.assign(interactiveMessage, m),
1051
+ }
1052
+
1053
+ if (hasNonNullishProperty(message, "title")) {
1054
+ interactiveMessage.header.title = message.title
1055
+ }
1056
+
1057
+ if (hasNonNullishProperty(message, "subtitle")) {
1058
+ interactiveMessage.header.subtitle = message.subtitle
1059
+ }
1060
+
1061
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) {
1062
+ interactiveMessage.header.hasMediaAttachment = Boolean(
1063
+ message.hasMediaAttachment
1064
+ )
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ if (hasNonNullishProperty(message, "footer")) {
1070
+ interactiveMessage.footer = {
1071
+ text: message.footer,
1072
+ }
1073
+ }
1074
+
1075
+ m = { interactiveMessage }
1076
+ } else if (hasNonNullishProperty(message, "shop")) {
1077
+ const interactiveMessage = {
1078
+ shopStorefrontMessage: {
1079
+ surface: message.shop.surface,
1080
+ id: message.shop.id,
1081
+ },
1082
+ }
1083
+
1084
+ if (hasNonNullishProperty(message, "text")) {
1085
+ interactiveMessage.body = {
1086
+ text: message.text,
1087
+ }
1088
+ }
1089
+
1090
+ if (hasNonNullishProperty(message, "title")) {
1091
+ interactiveMessage.header = {
1092
+ title: message.title,
1093
+ subtitle: null,
1094
+ hasMediaAttachment: false,
1095
+ }
1096
+
1097
+ if (hasNonNullishProperty(message, "subtitle")) {
1098
+ interactiveMessage.header.subtitle = message.subtitle
1099
+ }
1100
+ } else {
1101
+ if (hasNonNullishProperty(message, "caption")) {
1102
+ interactiveMessage.body = {
1103
+ text: message.caption,
1104
+ }
1105
+
1106
+ interactiveMessage.header = {
1107
+ title: null,
1108
+ subtitle: null,
1109
+ hasMediaAttachment: false,
1110
+ ...Object.assign(interactiveMessage, m),
1111
+ }
1112
+
1113
+ if (hasNonNullishProperty(message, "title")) {
1114
+ interactiveMessage.header.title = message.title
1115
+ }
1116
+
1117
+ if (hasNonNullishProperty(message, "subtitle")) {
1118
+ interactiveMessage.header.subtitle = message.subtitle
1119
+ }
1120
+
1121
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) {
1122
+ interactiveMessage.header.hasMediaAttachment = Boolean(
1123
+ message.hasMediaAttachment
1124
+ )
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ if (hasNonNullishProperty(message, "footer")) {
1130
+ interactiveMessage.footer = {
1131
+ text: message.footer,
1132
+ }
1133
+ }
1134
+
1135
+ m = { interactiveMessage }
1136
+ } else if (hasNonNullishProperty(message, "collection")) {
1137
+ const interactiveMessage = {
1138
+ collectionMessage: {
1139
+ bizJid: message.collection.bizJid,
1140
+ id: message.collection.id,
1141
+ messageVersion: message?.collection?.version,
1142
+ },
1143
+ }
1144
+
1145
+ if (hasNonNullishProperty(message, "text")) {
1146
+ interactiveMessage.body = {
1147
+ text: message.text,
1148
+ }
1149
+ }
1150
+
1151
+ if (hasNonNullishProperty(message, "title")) {
1152
+ interactiveMessage.header = {
1153
+ title: message.title,
1154
+ subtitle: null,
1155
+ hasMediaAttachment: false,
1156
+ }
1157
+
1158
+ if (hasNonNullishProperty(message, "subtitle")) {
1159
+ interactiveMessage.header.subtitle = message.subtitle
1160
+ }
1161
+ } else {
1162
+ if (hasNonNullishProperty(message, "caption")) {
1163
+ interactiveMessage.body = {
1164
+ text: message.caption,
1165
+ }
1166
+
1167
+ interactiveMessage.header = {
1168
+ title: null,
1169
+ subtitle: null,
1170
+ hasMediaAttachment: false,
1171
+ ...Object.assign(interactiveMessage, m),
1172
+ }
1173
+
1174
+ if (hasNonNullishProperty(message, "title")) {
1175
+ interactiveMessage.header.title = message.title
1176
+ }
1177
+
1178
+ if (hasNonNullishProperty(message, "subtitle")) {
1179
+ interactiveMessage.header.subtitle = message.subtitle
1180
+ }
1181
+
1182
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) {
1183
+ interactiveMessage.header.hasMediaAttachment = Boolean(
1184
+ message.hasMediaAttachment
1185
+ )
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ if (hasNonNullishProperty(message, "footer")) {
1191
+ interactiveMessage.footer = {
1192
+ text: message.footer,
1193
+ }
1194
+ }
1195
+
1196
+ m = { interactiveMessage }
1197
+ } else if (hasNonNullishProperty(message, "cards")) {
1198
+ const slides = await Promise.all(
1199
+ message.cards.map(async (slide) => {
1200
+ const {
1201
+ image,
1202
+ video,
1203
+ product,
1204
+ title,
1205
+ body,
1206
+ footer,
1207
+ buttons,
1208
+ } = slide
1209
+
1210
+ let header
1211
+
1212
+ if (product) {
1213
+ const { imageMessage } = await prepareWAMessageMedia(
1214
+ { image: product.productImage, ...options },
1215
+ options
1216
+ )
1217
+ header = {
1218
+ productMessage: {
1219
+ product: {
1220
+ ...product,
1221
+ productImage: imageMessage,
1222
+ },
1223
+ ...slide,
1224
+ },
1225
+ }
1226
+ } else if (image) {
1227
+ header = await prepareWAMessageMedia(
1228
+ { image: image, ...options },
1229
+ options
1230
+ )
1231
+ } else if (video) {
1232
+ header = await prepareWAMessageMedia(
1233
+ { video: video, ...options },
1234
+ options
1235
+ )
1236
+ }
1237
+
1238
+ const msg = {
1239
+ header: {
1240
+ title,
1241
+ hasMediaAttachment: true,
1242
+ ...header,
1243
+ },
1244
+ body: {
1245
+ text: body,
1246
+ },
1247
+ footer: {
1248
+ text: footer,
1249
+ },
1250
+ nativeFlowMessage: {
1251
+ buttons,
1252
+ },
1253
+ }
1254
+
1255
+ return msg
1256
+ })
1257
+ )
1258
+
1259
+ const interactiveMessage = {
1260
+ carouselMessage: {
1261
+ cards: slides,
1262
+ },
1263
+ }
1264
+
1265
+ if (hasNonNullishProperty(message, "text")) {
1266
+ interactiveMessage.body = {
1267
+ text: message.text,
1268
+ }
1269
+ }
1270
+
1271
+ if (hasNonNullishProperty(message, "title")) {
1272
+ interactiveMessage.header = {
1273
+ title: message.title,
1274
+ subtitle: null,
1275
+ hasMediaAttachment: false,
1276
+ }
1277
+
1278
+ if (hasNonNullishProperty(message, "subtitle")) {
1279
+ interactiveMessage.header.subtitle = message.subtitle
1280
+ }
1281
+ } else {
1282
+ if (hasNonNullishProperty(message, "caption")) {
1283
+ interactiveMessage.body = {
1284
+ text: message.caption,
1285
+ }
1286
+
1287
+ interactiveMessage.header = {
1288
+ title: null,
1289
+ subtitle: null,
1290
+ hasMediaAttachment: false,
1291
+ ...Object.assign(interactiveMessage, m),
1292
+ }
1293
+
1294
+ if (hasNonNullishProperty(message, "title")) {
1295
+ interactiveMessage.header.title = message.title
1296
+ }
1297
+
1298
+ if (hasNonNullishProperty(message, "subtitle")) {
1299
+ interactiveMessage.header.subtitle = message.subtitle
1300
+ }
1301
+
1302
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) {
1303
+ interactiveMessage.header.hasMediaAttachment = Boolean(
1304
+ message.hasMediaAttachment
1305
+ )
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ if (hasNonNullishProperty(message, "footer")) {
1311
+ interactiveMessage.footer = {
1312
+ text: message.footer,
1313
+ }
1314
+ }
1315
+
1316
+ m = { interactiveMessage }
1317
+ }
1318
+
1319
+ if (hasOptionalProperty(message, "ephemeral")) {
1320
+ m = { ephemeralMessage: { message: m } }
1321
+ }
1322
+
1323
+ if (hasOptionalProperty(message, "mentions") && message.mentions?.length) {
1324
+ const messageType = Object.keys(m)[0]
1325
+ const key = m[messageType]
1326
+
1327
+ if ("contextInfo" in key && !!key.contextInfo) {
1328
+ key.contextInfo.mentionedJid = message.mentions
1329
+ } else if (key) {
1330
+ key.contextInfo = {
1331
+ mentionedJid: message.mentions,
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ if (hasOptionalProperty(message, "contextInfo") && !!message.contextInfo) {
1337
+ const messageType = Object.keys(m)[0]
1338
+ const key = m[messageType]
1339
+
1340
+ if ("contextInfo" in key && !!key.contextInfo) {
1341
+ key.contextInfo = { ...key.contextInfo, ...message.contextInfo }
1342
+ } else if (key) {
1343
+ key.contextInfo = message.contextInfo
1344
+ }
1345
+ }
1346
+
1347
+ if (hasOptionalProperty(message, "edit")) {
1348
+ m = {
1349
+ protocolMessage: {
1350
+ key: message.edit,
1351
+ editedMessage: m,
1352
+ timestampMs: Date.now(),
1353
+ type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
1354
+ },
1355
+ }
1356
+ }
1357
+
1358
+ return WAProto.Message.fromObject(m)
1359
+ }
1360
+
1361
+ const generateWAMessageFromContent = (jid, message, options) => {
1362
+ if (!options.timestamp) {
1363
+ options.timestamp = new Date()
1364
+ }
1365
+
1366
+ const innerMessage = normalizeMessageContent(message)
1367
+ const key = getContentType(innerMessage)
1368
+ const timestamp = unixTimestampSeconds(options.timestamp)
1369
+ const threadId = []
1370
+ const { quoted, userJid } = options
1371
+
1372
+ if (quoted && !isJidNewsletter(jid)) {
1373
+ const participant = quoted.key.fromMe
1374
+ ? userJid
1375
+ : quoted.participant || quoted.key.participant || quoted.key.remoteJid
1376
+
1377
+ let quotedMsg = normalizeMessageContent(quoted.message)
1378
+ const msgType = getContentType(quotedMsg)
1379
+
1380
+ quotedMsg = proto.Message.fromObject({ [msgType]: quotedMsg[msgType] })
1381
+
1382
+ const quotedContent = quotedMsg[msgType]
1383
+
1384
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
1385
+ delete quotedContent.contextInfo
1386
+ }
1387
+
1388
+ let requestPayment
1389
+
1390
+ if (key === 'requestPaymentMessage') {
1391
+ if (innerMessage?.requestPaymentMessage && innerMessage?.requestPaymentMessage?.noteMessage?.extendedTextMessage) {
1392
+ requestPayment = innerMessage?.requestPaymentMessage?.noteMessage?.extendedTextMessage
1393
+ } else if (innerMessage?.requestPaymentMessage && innerMessage?.requestPaymentMessage?.noteMessage?.stickerMessage) {
1394
+ requestPayment = innerMessage.requestPaymentMessage?.noteMessage?.stickerMessage
1395
+ }
1396
+ }
1397
+
1398
+ const contextInfo = (key === 'requestPaymentMessage' ? requestPayment?.contextInfo : innerMessage[key].contextInfo) || {}
1399
+
1400
+ contextInfo.participant = jidNormalizedUser(participant)
1401
+ contextInfo.stanzaId = quoted.key.id
1402
+ contextInfo.quotedMessage = quotedMsg
1403
+
1404
+ if (jid !== quoted.key.remoteJid) {
1405
+ contextInfo.remoteJid = quoted.key.remoteJid
1406
+ }
1407
+
1408
+ if (contextInfo.quotedMessage) {
1409
+ contextInfo.quotedType = 0
1410
+ }
1411
+
1412
+ if (contextInfo.quotedMessage && isJidGroup(jid)) {
1413
+ threadId.push({
1414
+ threadType: proto.ThreadID.ThreadType.VIEW_REPLIES,
1415
+ threadKey: {
1416
+ remoteJid: quoted?.key?.remoteJid,
1417
+ fromMe: quoted?.key?.fromMe,
1418
+ id: generateMessageID(),
1419
+ ...(quoted?.key?.fromMe ? {} : { participant: quoted?.key?.participant })
1420
+ }
1421
+ })
1422
+ }
1423
+
1424
+ if (key === 'requestPaymentMessage' && requestPayment) {
1425
+ requestPayment.contextInfo = contextInfo
1426
+ } else {
1427
+ innerMessage[key].contextInfo = contextInfo
1428
+ }
1429
+ }
1430
+
1431
+ if (key !== 'protocolMessage' &&
1432
+ key !== 'ephemeralMessage' &&
1433
+ !isJidNewsletter(jid)) {
1434
+ message.messageContextInfo = {
1435
+ threadId: threadId.length > 0 ? threadId : [],
1436
+ messageSecret: randomBytes(32),
1437
+ ...message.messageContextInfo
1438
+ }
1439
+ innerMessage[key].contextInfo = {
1440
+ ...(innerMessage[key].contextInfo || {}),
1441
+ expiration: options.ephemeralExpiration ? options.ephemeralExpiration : 0
1442
+ }
1443
+ }
1444
+
1445
+ message = WAProto.Message.fromObject(message)
1446
+
1447
+ const messageJSON = {
1448
+ key: {
1449
+ remoteJid: jid,
1450
+ fromMe: true,
1451
+ id: options?.messageId || generateMessageID()
1452
+ },
1453
+ message: message,
1454
+ messageTimestamp: timestamp,
1455
+ messageStubParameters: [],
1456
+ participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
1457
+ status: WAMessageStatus.PENDING
1458
+ }
1459
+
1460
+ return WAProto.WebMessageInfo.fromObject(messageJSON)
1461
+ }
1462
+
1463
+ const generateWAMessage = async (jid, content, options) => {
1464
+ options.logger = options?.logger?.child({ msgId: options.messageId })
1465
+
1466
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { newsletter: isJidNewsletter(jid), ...options }), options)
1467
+ }
1468
+
1469
+ const getContentType = (content) => {
1470
+ if (content) {
1471
+ const keys = Object.keys(content)
1472
+ const key = keys.find(k => (k === 'conversation' || k.endsWith('Message') || k.endsWith('V2') || k.endsWith('V3') || k.endsWith('V4')) && k !== 'senderKeyDistributionMessage' && k !== 'messageContextInfo')
1473
+
1474
+ return key
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * Normalizes ephemeral, view once messages to regular message content
1480
+ * Eg. image messages in ephemeral messages, in view once messages etc.
1481
+ * @param content
1482
+ * @returns
1483
+ */
1484
+ const normalizeMessageContent = (content) => {
1485
+ if (!content) {
1486
+ return undefined
1487
+ }
1488
+
1489
+ for (let i = 0; i < 5; i++) {
1490
+ const inner = getFutureProofMessage(content)
1491
+ if (!inner) {
1492
+ break
1493
+ }
1494
+
1495
+ content = inner.message
1496
+ }
1497
+
1498
+ return content
1499
+
1500
+ function getFutureProofMessage(message) {
1501
+ return (
1502
+ (message?.editedMessage)
1503
+ || (message?.statusAddYours)
1504
+ || (message?.botTaskMessage)
1505
+ || (message?.eventCoverImage)
1506
+ || (message?.questionMessage)
1507
+ || (message?.viewOnceMessage)
1508
+ || (message?.botInvokeMessage)
1509
+ || (message?.ephemeralMessage)
1510
+ || (message?.limitSharingMessage)
1511
+ || (message?.viewOnceMessageV2)
1512
+ || (message?.lottieStickerMessage)
1513
+ || (message?.groupStatusMessage)
1514
+ || (message?.questionReplyMessage)
1515
+ || (message?.botForwardedMessage)
1516
+ || (message?.statusMentionMessage)
1517
+ || (message?.groupStatusMessageV2)
1518
+ || (message?.pollCreationMessageV4)
1519
+ || (message?.associatedChildMessage)
1520
+ || (message?.groupMentionedMessage)
1521
+ || (message?.groupStatusMentionMessage)
1522
+ || (message?.viewOnceMessageV2Extension)
1523
+ || (message?.documentWithCaptionMessage)
1524
+ || (message?.pollCreationOptionImageMessage))
1525
+ }
1526
+ }
1527
+
1528
+ /**
1529
+ * Extract the true message content from a message
1530
+ * Eg. extracts the inner message from a disappearing message/view once message
1531
+ */
1532
+ const extractMessageContent = (content) => {
1533
+ const extractFromButtonsMessage = (msg) => {
1534
+ const header = typeof msg.header === 'object' && msg.header !== null
1535
+
1536
+ if (header ? msg.header?.imageMessage : msg.imageMessage) {
1537
+ return { imageMessage: header ? msg.header.imageMessage : msg.imageMessage }
1538
+ }
1539
+
1540
+ else if (header ? msg.header?.documentMessage : msg.documentMessage) {
1541
+ return { documentMessage: header ? msg.header.documentMessage : msg.documentMessage }
1542
+ }
1543
+
1544
+ else if (header ? msg.header?.videoMessage : msg.videoMessage) {
1545
+ return { videoMessage: header ? msg.header.videoMessage: msg.videoMessage }
1546
+ }
1547
+
1548
+ else if (header ? msg.header?.locationMessage : msg.locationMessage) {
1549
+ return { locationMessage: header ? msg.header.locationMessage : msg.locationMessage }
1550
+ }
1551
+
1552
+ else if (header ? msg.header?.productMessage : msg.productMessage) {
1553
+ return { productMessage: header ? msg.header.productMessage : msg.productMessage }
1554
+ }
1555
+
1556
+ else {
1557
+ return {
1558
+ conversation: 'contentText' in msg
1559
+ ? msg.contentText
1560
+ : ('hydratedContentText' in msg ? msg.hydratedContentText : 'body' in msg ? msg.body.text : '')
1561
+ }
1562
+ }
1563
+ }
1564
+
1565
+ content = normalizeMessageContent(content)
1566
+
1567
+ if (content?.buttonsMessage) {
1568
+ return extractFromButtonsMessage(content.buttonsMessage)
1569
+ }
1570
+
1571
+ if (content?.interactiveMessage) {
1572
+ return extractFromButtonsMessage(content.interactiveMessage)
1573
+ }
1574
+
1575
+ if (content?.templateMessage?.interactiveMessageTemplate) {
1576
+ return extractFromButtonsMessage(content?.templateMessage?.interactiveMessageTemplate)
1577
+ }
1578
+
1579
+ if (content?.templateMessage?.hydratedFourRowTemplate) {
1580
+ return extractFromButtonsMessage(content?.templateMessage?.hydratedFourRowTemplate)
1581
+ }
1582
+
1583
+ if (content?.templateMessage?.hydratedTemplate) {
1584
+ return extractFromButtonsMessage(content?.templateMessage?.hydratedTemplate)
1585
+ }
1586
+
1587
+ if (content?.templateMessage?.fourRowTemplate) {
1588
+ return extractFromButtonsMessage(content?.templateMessage?.fourRowTemplate)
1589
+ }
1590
+
1591
+ return content
1592
+ }
1593
+
1594
+ /**
1595
+ * Returns the device predicted by message ID
1596
+ */
1597
+ const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' :
1598
+ /^3E.{20}$/.test(id) ? 'web' :
1599
+ /^(.{21}|.{32})$/.test(id) ? 'android' :
1600
+ /^(3F|.{18}$)/.test(id) ? 'desktop' :
1601
+ 'baileys'
1602
+
1603
+ /** Upserts a receipt in the message */
1604
+ const updateMessageWithReceipt = (msg, receipt) => {
1605
+ msg.userReceipt = msg.userReceipt || []
1606
+ const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid)
1607
+
1608
+ if (recp) {
1609
+ Object.assign(recp, receipt)
1610
+ }
1611
+
1612
+ else {
1613
+ msg.userReceipt.push(receipt)
1614
+ }
1615
+ }
1616
+
1617
+ /** Update the message with a new reaction */
1618
+ const updateMessageWithReaction = (msg, reaction) => {
1619
+ const authorID = getKeyAuthor(reaction.key)
1620
+ const reactions = (msg.reactions || [])
1621
+ .filter(r => getKeyAuthor(r.key) !== authorID)
1622
+
1623
+ reaction.text = reaction.text || ''
1624
+ reactions.push(reaction)
1625
+ msg.reactions = reactions
1626
+ }
1627
+
1628
+ /** Update the message with a new poll update */
1629
+ const updateMessageWithPollUpdate = (msg, update) => {
1630
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey)
1631
+ const votes = (msg.pollUpdates || [])
1632
+ .filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
1633
+
1634
+ if (update.vote?.selectedOptions?.length) {
1635
+ votes.push(update)
1636
+ }
1637
+
1638
+ msg.pollUpdates = votes
1639
+ }
1640
+
1641
+ /** Update the message with a new event response*/
1642
+ const updateMessageWithEventResponse = (msg, update) => {
1643
+ const authorID = getKeyAuthor(update.eventResponseMessageKey)
1644
+ const responses = (msg.eventResponses || [])
1645
+ .filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID)
1646
+
1647
+ responses.push(update)
1648
+ msg.eventResponses = responses
1649
+ }
1650
+
1651
+ /**
1652
+ * Aggregates all poll updates in a poll.
1653
+ * @param msg the poll creation message
1654
+ * @param meId your jid
1655
+ * @returns A list of options & their voters
1656
+ */
1657
+ function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
1658
+ message = normalizeMessageContent(message)
1659
+
1660
+ const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
1661
+
1662
+ const voteHashMap = opts.reduce((acc, opt) => {
1663
+ const hash = sha256(Buffer.from(opt.optionName || '')).toString()
1664
+ acc[hash] = {
1665
+ name: opt.optionName || '',
1666
+ voters: []
1667
+ }
1668
+
1669
+ return acc
1670
+ }, {})
1671
+
1672
+ for (const update of pollUpdates || []) {
1673
+ const { vote } = update
1674
+
1675
+ if (!vote) {
1676
+ continue
1677
+ }
1678
+
1679
+ for (const option of vote.selectedOptions || []) {
1680
+ const hash = option.toString()
1681
+ let data = voteHashMap[hash]
1682
+
1683
+ if (!data) {
1684
+ voteHashMap[hash] = {
1685
+ name: 'Unknown',
1686
+ voters: []
1687
+ }
1688
+
1689
+ data = voteHashMap[hash]
1690
+ }
1691
+
1692
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId))
1693
+ }
1694
+ }
1695
+
1696
+ return Object.values(voteHashMap)
1697
+ }
1698
+
1699
+ /**
1700
+ * Aggregates all event responses in an event message.
1701
+ * @param msg the event creation message
1702
+ * @param meLid your lid
1703
+ * @returns A list of response types & their responders
1704
+ */
1705
+ function getAggregateResponsesInEventMessage({ eventResponses }, meLid) {
1706
+ const responseTypes = ['GOING', 'NOT_GOING', 'MAYBE']
1707
+ const responseMap = {}
1708
+
1709
+ for (const type of responseTypes) {
1710
+ responseMap[type] = {
1711
+ response: type,
1712
+ responders: []
1713
+ }
1714
+ }
1715
+
1716
+ for (const update of eventResponses) {
1717
+ const { response } = update.response || 0
1718
+ const responseType = proto.Message.EventResponseMessage.EventResponseType[response]
1719
+ if (responseType !== 'UNKNOWN' && responseMap[responseType]) {
1720
+ responseMap[responseType].responders.push(getKeyAuthor(update.eventResponseMessageKey, meLid))
1721
+ }
1722
+ }
1723
+
1724
+ return Object.values(responseMap)
1725
+ }
1726
+
1727
+ /** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
1728
+ const aggregateMessageKeysNotFromMe = (keys) => {
1729
+ const keyMap = {}
1730
+
1731
+ for (const { remoteJid, id, participant, fromMe } of keys) {
1732
+ if (!fromMe) {
1733
+ const uqKey = `${remoteJid}:${participant || ''}`
1734
+
1735
+ if (!keyMap[uqKey]) {
1736
+ keyMap[uqKey] = {
1737
+ jid: remoteJid,
1738
+ participant: participant,
1739
+ messageIds: []
1740
+ }
1741
+ }
1742
+
1743
+ keyMap[uqKey].messageIds.push(id)
1744
+ }
1745
+ }
1746
+
1747
+ return Object.values(keyMap)
1748
+ }
1749
+
1750
+ const REUPLOAD_REQUIRED_STATUS = [410, 404]
1751
+
1752
+ /**
1753
+ * Downloads the given message. Throws an error if it's not a media message
1754
+ */
1755
+ const downloadMediaMessage = async (message, type, options, ctx) => {
1756
+ const result = await downloadMsg().catch(async (error) => {
1757
+ if (ctx &&
1758
+ typeof error?.status === 'number' && // treat errors with status as HTTP failures requiring reupload
1759
+ REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
1760
+ ctx.logger.info({ key: message.key }, 'sending reupload media request...')
1761
+
1762
+ // request reupload
1763
+ message = await ctx.reuploadRequest(message)
1764
+
1765
+ const result = await downloadMsg()
1766
+ return result
1767
+ }
1768
+
1769
+ throw error
1770
+ })
1771
+
1772
+ return result
1773
+
1774
+ async function downloadMsg() {
1775
+ const mContent = extractMessageContent(message.message)
1776
+
1777
+ if (!mContent) {
1778
+ throw new Boom('No message present', { statusCode: 400, data: message })
1779
+ }
1780
+
1781
+ const contentType = getContentType(mContent)
1782
+
1783
+ let mediaType = contentType?.replace('Message', '')
1784
+
1785
+ const media = contentType === 'productMessage' ? mContent[contentType]?.product?.productImage : mContent[contentType]
1786
+
1787
+ if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
1788
+ throw new Boom(`"${contentType}" message is not a media message`)
1789
+ }
1790
+
1791
+ let download
1792
+
1793
+ if ('thumbnailDirectPath' in media && !('url' in media)) {
1794
+ download = {
1795
+ directPath: media.thumbnailDirectPath,
1796
+ mediaKey: media.mediaKey
1797
+ }
1798
+ mediaType = 'thumbnail-link'
1799
+ }
1800
+ else {
1801
+ download = media
1802
+ }
1803
+
1804
+ const stream = await downloadContentFromMessage(download, mediaType, options)
1805
+
1806
+ if (type === 'buffer') {
1807
+ const bufferArray = []
1808
+ for await (const chunk of stream) {
1809
+ bufferArray.push(chunk);
1810
+ }
1811
+ return Buffer.concat(bufferArray);
1812
+ }
1813
+ return stream
1814
+ }
1815
+ }
1816
+
1817
+ /** Checks whether the given message is a media message; if it is returns the inner content */
1818
+ const assertMediaContent = (content) => {
1819
+ content = extractMessageContent(content)
1820
+
1821
+ const mediaContent = content?.documentMessage ||
1822
+ content?.imageMessage ||
1823
+ content?.videoMessage ||
1824
+ content?.audioMessage ||
1825
+ content?.stickerMessage
1826
+
1827
+ if (!mediaContent) {
1828
+ throw new Boom('given message is not a media message', { statusCode: 400, data: content });
1829
+ }
1830
+
1831
+ return mediaContent
1832
+ }
1833
+
1834
+ module.exports = {
1835
+ extractUrlFromText,
1836
+ generateLinkPreviewIfRequired,
1837
+ prepareWAMessageMedia,
1838
+ prepareAlbumMessageContent,
1839
+ prepareDisappearingMessageSettingContent,
1840
+ generateForwardMessageContent,
1841
+ generateWAMessageContent,
1842
+ generateWAMessageFromContent,
1843
+ generateWAMessage,
1844
+ getContentType,
1845
+ hasNonNullishProperty,
1846
+ normalizeMessageContent,
1847
+ extractMessageContent,
1848
+ getDevice,
1849
+ updateMessageWithReceipt,
1850
+ updateMessageWithReaction,
1851
+ updateMessageWithPollUpdate,
1852
+ updateMessageWithEventResponse,
1853
+ getAggregateVotesInPollMessage,
1854
+ getAggregateResponsesInEventMessage,
1855
+ aggregateMessageKeysNotFromMe,
1856
+ downloadMediaMessage,
1857
+ assertMediaContent
1858
+ }