vinnleys 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.

Potentially problematic release.


This version of vinnleys might be problematic. Click here for more details.

Files changed (127) hide show
  1. package/README.md +108 -0
  2. package/WAProto/GenerateStatics.sh +3 -0
  3. package/WAProto/WAProto.proto +6922 -0
  4. package/WAProto/fix-imports.js +85 -0
  5. package/WAProto/index.d.ts +79257 -0
  6. package/WAProto/index.js +242946 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Defaults/index.js +131 -0
  9. package/lib/Signal/Group/ciphertext-message.js +12 -0
  10. package/lib/Signal/Group/group-session-builder.js +30 -0
  11. package/lib/Signal/Group/group_cipher.js +82 -0
  12. package/lib/Signal/Group/index.js +12 -0
  13. package/lib/Signal/Group/keyhelper.js +18 -0
  14. package/lib/Signal/Group/sender-chain-key.js +26 -0
  15. package/lib/Signal/Group/sender-key-distribution-message.js +63 -0
  16. package/lib/Signal/Group/sender-key-message.js +66 -0
  17. package/lib/Signal/Group/sender-key-name.js +48 -0
  18. package/lib/Signal/Group/sender-key-record.js +41 -0
  19. package/lib/Signal/Group/sender-key-state.js +84 -0
  20. package/lib/Signal/Group/sender-message-key.js +26 -0
  21. package/lib/Signal/libsignal.js +431 -0
  22. package/lib/Signal/lid-mapping.js +277 -0
  23. package/lib/Socket/Client/index.js +3 -0
  24. package/lib/Socket/Client/types.js +11 -0
  25. package/lib/Socket/Client/websocket.js +54 -0
  26. package/lib/Socket/MessageBuilder.js +3728 -0
  27. package/lib/Socket/business.js +379 -0
  28. package/lib/Socket/chats.js +1198 -0
  29. package/lib/Socket/communities.js +431 -0
  30. package/lib/Socket/graphql.js +716 -0
  31. package/lib/Socket/groups.js +374 -0
  32. package/lib/Socket/index.js +24 -0
  33. package/lib/Socket/interop.js +463 -0
  34. package/lib/Socket/luxu.js +387 -0
  35. package/lib/Socket/message-builder.js +521 -0
  36. package/lib/Socket/messages-recv.js +1916 -0
  37. package/lib/Socket/messages-send.js +1511 -0
  38. package/lib/Socket/mex.js +54 -0
  39. package/lib/Socket/newsletter.js +636 -0
  40. package/lib/Socket/privacy.js +318 -0
  41. package/lib/Socket/socket.js +1114 -0
  42. package/lib/Socket/username.js +236 -0
  43. package/lib/Store/index.js +10 -0
  44. package/lib/Store/keyed-db.js +108 -0
  45. package/lib/Store/make-cache-manager-store.js +85 -0
  46. package/lib/Store/make-in-memory-store.js +244 -0
  47. package/lib/Store/make-ordered-dictionary.js +75 -0
  48. package/lib/Store/object-repository.js +32 -0
  49. package/lib/Types/Auth.js +2 -0
  50. package/lib/Types/Bussines.js +2 -0
  51. package/lib/Types/Call.js +2 -0
  52. package/lib/Types/Chat.js +8 -0
  53. package/lib/Types/Contact.js +2 -0
  54. package/lib/Types/Events.js +2 -0
  55. package/lib/Types/GroupMetadata.js +2 -0
  56. package/lib/Types/Label.js +25 -0
  57. package/lib/Types/LabelAssociation.js +7 -0
  58. package/lib/Types/Message.js +11 -0
  59. package/lib/Types/Mex.js +111 -0
  60. package/lib/Types/Product.js +2 -0
  61. package/lib/Types/Signal.js +2 -0
  62. package/lib/Types/Socket.js +3 -0
  63. package/lib/Types/State.js +56 -0
  64. package/lib/Types/USync.js +2 -0
  65. package/lib/Types/index.js +26 -0
  66. package/lib/Utils/auth-utils.js +302 -0
  67. package/lib/Utils/browser-utils.js +48 -0
  68. package/lib/Utils/business.js +231 -0
  69. package/lib/Utils/chat-utils.js +872 -0
  70. package/lib/Utils/companion-reg-client-utils.js +35 -0
  71. package/lib/Utils/crypto.js +118 -0
  72. package/lib/Utils/decode-wa-message.js +350 -0
  73. package/lib/Utils/event-buffer.js +622 -0
  74. package/lib/Utils/generics.js +403 -0
  75. package/lib/Utils/history.js +134 -0
  76. package/lib/Utils/identity-change-handler.js +50 -0
  77. package/lib/Utils/index.js +24 -0
  78. package/lib/Utils/link-preview.js +85 -0
  79. package/lib/Utils/logger.js +3 -0
  80. package/lib/Utils/lt-hash.js +8 -0
  81. package/lib/Utils/make-mutex.js +33 -0
  82. package/lib/Utils/message-composer.js +273 -0
  83. package/lib/Utils/message-retry-manager.js +265 -0
  84. package/lib/Utils/messages-media.js +870 -0
  85. package/lib/Utils/messages.js +1394 -0
  86. package/lib/Utils/noise-handler.js +201 -0
  87. package/lib/Utils/offline-node-processor.js +40 -0
  88. package/lib/Utils/pre-key-manager.js +106 -0
  89. package/lib/Utils/process-message.js +630 -0
  90. package/lib/Utils/reporting-utils.js +258 -0
  91. package/lib/Utils/signal.js +201 -0
  92. package/lib/Utils/stanza-ack.js +38 -0
  93. package/lib/Utils/sticker.js +133 -0
  94. package/lib/Utils/sync-action-utils.js +49 -0
  95. package/lib/Utils/tc-token-utils.js +163 -0
  96. package/lib/Utils/use-multi-file-auth-state.js +121 -0
  97. package/lib/Utils/use-sqlite-auth-state.js +162 -0
  98. package/lib/Utils/validate-connection.js +203 -0
  99. package/lib/WABinary/constants.js +1301 -0
  100. package/lib/WABinary/decode.js +262 -0
  101. package/lib/WABinary/encode.js +220 -0
  102. package/lib/WABinary/generic-utils.js +204 -0
  103. package/lib/WABinary/index.js +6 -0
  104. package/lib/WABinary/jid-utils.js +98 -0
  105. package/lib/WABinary/types.js +2 -0
  106. package/lib/WAM/BinaryInfo.js +10 -0
  107. package/lib/WAM/constants.js +22853 -0
  108. package/lib/WAM/encode.js +150 -0
  109. package/lib/WAM/index.js +4 -0
  110. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +53 -0
  111. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  112. package/lib/WAUSync/Protocols/USyncContactProtocol.js +54 -0
  113. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +57 -0
  114. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +30 -0
  115. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +58 -0
  116. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +30 -0
  117. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
  118. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +28 -0
  119. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +39 -0
  120. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
  121. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
  122. package/lib/WAUSync/Protocols/index.js +12 -0
  123. package/lib/WAUSync/USyncQuery.js +151 -0
  124. package/lib/WAUSync/USyncUser.js +56 -0
  125. package/lib/WAUSync/index.js +3 -0
  126. package/lib/index.js +33 -0
  127. package/package.json +135 -0
@@ -0,0 +1,521 @@
1
+ import crypto from 'crypto';
2
+ import { proto } from '../../WAProto/index.js';
3
+ import { generateWAMessageFromContent, prepareWAMessageMedia } from '../Utils/index.js';
4
+
5
+ export const makeMessageBuilderSocket = (sock) => {
6
+ const { relayMessage, sendMessage, waUploadToServer } = sock;
7
+
8
+ // per-socket-instance poll state: id -> [{ vote, action }]
9
+ const pollActionStore = new Map();
10
+
11
+ /**
12
+ * Relay an arbitrary raw message content object, bypassing the normal
13
+ * content-type detection in sendMessage(). Useful when you've already
14
+ * built a proto-shaped message (e.g. from generateWAMessageFromContent)
15
+ * and just need it sent as-is.
16
+ */
17
+ const sendJsonMessage = async (jid, content = {}, options = {}) => {
18
+ const msg = generateWAMessageFromContent(jid, content, {});
19
+ return relayMessage(jid, msg.message, { messageId: msg.key.id, ...options });
20
+ };
21
+
22
+ /**
23
+ * Sends an HTML GenAI primitive rich response message.
24
+ * html: string template/raw HTML markup to be rendered.
25
+ */
26
+ const sendHtml = async (jid, html = '', options = {}) => {
27
+ const payloadData = Buffer.from(
28
+ JSON.stringify({
29
+ __typename: "GenAIUnifiedResponse",
30
+ response_id: crypto.randomUUID(),
31
+ sections: [
32
+ {
33
+ __typename: "GenAIUnifiedResponseSection",
34
+ view_model: {
35
+ __typename: "GenAISingleLayoutViewModel",
36
+ primitive: {
37
+ __typename: "FOAHtmlPrimitiveDemoDONOTUSE",
38
+ trusted_sources: [],
39
+ payload: String(html).trim()
40
+ }
41
+ }
42
+ }
43
+ ]
44
+ })
45
+ ).toString("base64");
46
+
47
+ const messageContent = {
48
+ botForwardedMessage: {
49
+ message: {
50
+ richResponseMessage: {
51
+ messageType: 1,
52
+ unifiedResponse: {
53
+ data: payloadData
54
+ },
55
+ contextInfo: {
56
+ isForwarded: true,
57
+ forwardOrigin: 4
58
+ }
59
+ }
60
+ }
61
+ }
62
+ };
63
+
64
+ return sendJsonMessage(jid, messageContent, options);
65
+ };
66
+
67
+ /**
68
+ * Sends a WA poll and remembers which "action" each option maps to, so
69
+ * a caller can look up what the voter selected once the vote comes back
70
+ * through a `messages.update` event (see resolvePollAction below).
71
+ *
72
+ * pollOptions: [{ vote: 'Option label', action: 'anything you want back' }]
73
+ */
74
+ const sendActionPoll = async (jid, name = '', pollOptions = [], options = {}) => {
75
+ const values = pollOptions.map(o => o.vote);
76
+ const pollMsg = await sendMessage(jid, { poll: { name, values, selectableCount: options.selectableCount || 1 } }, options);
77
+ pollActionStore.set(pollMsg.key.id, pollOptions);
78
+ return pollMsg;
79
+ };
80
+
81
+ /**
82
+ * Given a poll creation message id and the selected option label (as
83
+ * resolved via getAggregateVotesInPollMessage), returns the `action`
84
+ * that was registered for it in sendActionPoll, or undefined if this
85
+ * poll wasn't created through sendActionPoll (or already forgotten).
86
+ */
87
+ const resolvePollAction = (pollMessageId, selectedVoteLabel) => {
88
+ const options = pollActionStore.get(pollMessageId);
89
+ return options?.find(o => o.vote === selectedVoteLabel)?.action;
90
+ };
91
+
92
+ /**
93
+ * Sends a set of media files as a single WA album (grouped gallery).
94
+ * media: array of local file paths or URLs; extension picks image/video.
95
+ */
96
+ const sendAlbumMessage = async (jid, media = [], contextInfo = {}) => {
97
+ const albumMsg = generateWAMessageFromContent(jid, proto.Message.fromObject({
98
+ albumMessage: {
99
+ expectedImageCount: media.filter(m => !/\.mp4$/i.test(m)).length,
100
+ expectedVideoCount: media.filter(m => /\.mp4$/i.test(m)).length,
101
+ contextInfo
102
+ }
103
+ }), {});
104
+
105
+ const albumKey = {
106
+ id: await relayMessage(jid, albumMsg.message, { messageId: albumMsg.key.id }),
107
+ remoteJid: jid,
108
+ fromMe: true
109
+ };
110
+
111
+ const keys = { album: albumKey };
112
+ let i = 1;
113
+
114
+ const mimetypes = {
115
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
116
+ gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4'
117
+ };
118
+ const messagetypes = {
119
+ jpg: 'imageMessage', jpeg: 'imageMessage', png: 'imageMessage',
120
+ gif: 'imageMessage', webp: 'imageMessage', mp4: 'videoMessage'
121
+ };
122
+
123
+ for (const source of media) {
124
+ const ext = source.split('.').pop().toLowerCase();
125
+ const mimetype = mimetypes[ext];
126
+ const type = messagetypes[ext];
127
+
128
+ if (!mimetype || !type) {
129
+ continue;
130
+ }
131
+
132
+ const msg = await prepareWAMessageMedia(
133
+ { [type.startsWith('image') ? 'image' : 'video']: { url: source }, mimetype },
134
+ { upload: waUploadToServer }
135
+ );
136
+
137
+ const mediaMessage = generateWAMessageFromContent(jid, proto.Message.fromObject({
138
+ associatedChildMessage: {
139
+ message: {
140
+ messageContextInfo: {
141
+ messageSecret: crypto.randomBytes(32),
142
+ messageAssociation: {
143
+ associationType: 'MEDIA_ALBUM',
144
+ parentMessageKey: albumKey
145
+ }
146
+ },
147
+ [type]: { ...msg[type] }
148
+ }
149
+ }
150
+ }), {});
151
+
152
+ keys[`media_${i++}`] = {
153
+ id: await relayMessage(jid, mediaMessage.message, { messageId: mediaMessage.key.id }),
154
+ fromMe: true,
155
+ remoteJid: jid
156
+ };
157
+ }
158
+
159
+ return keys;
160
+ };
161
+
162
+ /**
163
+ * Posts `content` to status@broadcast and mentions `jid` in it (i.e. a
164
+ * status update that notifies a specific contact).
165
+ */
166
+ const sendStatusMention = async (jid, content) => {
167
+ const media = generateWAMessageFromContent('status@broadcast', content, {});
168
+
169
+ const additionalNodes = [
170
+ {
171
+ tag: 'meta',
172
+ attrs: {},
173
+ content: [
174
+ {
175
+ tag: 'mentioned_users',
176
+ attrs: {},
177
+ content: [{ tag: 'to', attrs: { jid }, content: undefined }]
178
+ }
179
+ ]
180
+ }
181
+ ];
182
+
183
+ await relayMessage('status@broadcast', media.message, {
184
+ messageId: media.key.id,
185
+ statusJidList: [jid, sock.user?.id],
186
+ additionalNodes
187
+ });
188
+
189
+ return media;
190
+ };
191
+
192
+ /**
193
+ * Builds and sends a "rich response" card (WA's GenAI-style unified
194
+ * response widget: optional header image/title, body buttons or a
195
+ * carousel/row of cards, and an optional footer link).
196
+ *
197
+ * content.header: { title, image: { url, mime_type, inline, width, height }, disclaimer, disclaimerText }
198
+ * content.body: { title, buttons: [labels] } OR { carousel|row: true, cards: [{ title, buttons, toast }] }
199
+ * content.footer: { text, url, image }
200
+ */
201
+ const sendRichResponse = async (jid, content = {}) => {
202
+ const header = content?.header;
203
+ const body = content?.body;
204
+ const footer = content?.footer;
205
+ let messageContextInfo = {};
206
+ const sections = [];
207
+
208
+ if (header) {
209
+ const { disclaimer = false, disclaimerText = ' ', image = { inline: false }, title = '' } = header ?? {};
210
+
211
+ if (disclaimer) {
212
+ messageContextInfo = { messageContextInfo: { botMetadata: { messageDisclaimerText: disclaimerText } } };
213
+ }
214
+
215
+ if (title) {
216
+ sections.push({
217
+ __typename: 'GenAIUnifiedResponseSection',
218
+ view_model: {
219
+ __typename: 'GenAISingleLayoutViewModel',
220
+ primitive: { __typename: 'FOATextPrimitive', text: '# ' + title }
221
+ }
222
+ });
223
+ }
224
+
225
+ if (image?.url) {
226
+ sections.push(image.inline ? {
227
+ __typename: 'GenAIUnifiedResponseSection',
228
+ view_model: {
229
+ __typename: 'GenAISingleLayoutViewModel',
230
+ primitive: {
231
+ __typename: 'GenAIMarkdownTextUXPrimitive',
232
+ text: '{{header}}.{{/header}}',
233
+ inline_entities: [{
234
+ __typename: 'GenAITextInlineEntity',
235
+ key: 'header',
236
+ metadata: {
237
+ __typename: 'GenAILatexItem',
238
+ latex_expression: '.',
239
+ font_height: 24,
240
+ padding: 4,
241
+ latex_image: {
242
+ __typename: 'GenAIMediaItem',
243
+ mime_type: image.mime_type || 'image/png',
244
+ url: image.url,
245
+ url_fallback: image.url,
246
+ width: image.width || 500,
247
+ height: image.height || 500,
248
+ expiration_timestamp_ms: Date.now() + 86400000
249
+ }
250
+ }
251
+ }]
252
+ }
253
+ }
254
+ } : {
255
+ __typename: 'GenAIUnifiedResponseSection',
256
+ view_model: {
257
+ __typename: 'GenAISingleLayoutViewModel',
258
+ primitive: {
259
+ __typename: 'GenAIImagePrimitive',
260
+ preview_image: { __typename: 'GenAIMediaItem', mime_type: image.mime_type || 'image/png', url: image.url },
261
+ full_image: { __typename: 'GenAIMediaItem', mime_type: image.mime_type || 'image/png', url: image.url }
262
+ }
263
+ }
264
+ });
265
+ }
266
+ }
267
+
268
+ if (body) {
269
+ const { cards = null, buttons = null, title = '', toast = '', carousel = false, row = false } = body ?? {};
270
+
271
+ if ((carousel || row) && cards?.length >= 1) {
272
+ sections.push({
273
+ __typename: 'GenAIUnifiedResponseSection',
274
+ view_model: {
275
+ primitives: cards.map((card, cardIndex) => ({
276
+ __typename: 'GenAI3PExtWidgetPrimitive',
277
+ header: { __typename: 'GenAI3PExtWidgetStandardHeader', title: card?.title || '' },
278
+ body: {
279
+ __typename: 'GenAI3PExtCalendarEventList',
280
+ ctas: (card?.buttons || []).map((text, buttonIndex) => ({
281
+ label: text,
282
+ state: 'PENDING',
283
+ kind: 'OTHER',
284
+ tool_call_id: `${cardIndex}${buttonIndex}`,
285
+ toast: { label: card?.toast || '', __typename: 'GenAI3PExtWidgetToast' },
286
+ __typename: 'GenAI3PExtWidgetCTA'
287
+ })),
288
+ sections: []
289
+ }
290
+ })),
291
+ __typename: carousel ? 'GenAIHScrollLayoutViewModel' : 'GenAIActionRowLayoutViewModel'
292
+ }
293
+ });
294
+ } else if (buttons?.length) {
295
+ sections.push({
296
+ __typename: 'GenAIUnifiedResponseSection',
297
+ view_model: {
298
+ primitive: {
299
+ __typename: 'GenAI3PExtWidgetPrimitive',
300
+ header: { __typename: 'GenAI3PExtWidgetStandardHeader', title: title || '' },
301
+ body: {
302
+ __typename: 'GenAI3PExtCalendarEventList',
303
+ ctas: buttons.map((text, buttonIndex) => ({
304
+ label: text,
305
+ state: 'PENDING',
306
+ kind: 'OTHER',
307
+ tool_call_id: `${buttonIndex}`,
308
+ toast: { label: toast, __typename: 'GenAI3PExtWidgetToast' },
309
+ __typename: 'GenAI3PExtWidgetCTA'
310
+ })),
311
+ sections: []
312
+ }
313
+ },
314
+ __typename: 'GenAISingleLayoutViewModel'
315
+ }
316
+ });
317
+ }
318
+ }
319
+
320
+ if (footer) {
321
+ const { text = '', url = '', image = {} } = footer ?? {};
322
+ const img = [];
323
+ if (image?.url) {
324
+ img.push({
325
+ __typename: 'GenAIMarkdownTextUXPrimitive',
326
+ text: '{{header}}.{{/header}}',
327
+ inline_entities: [{
328
+ __typename: 'GenAITextInlineEntity',
329
+ key: 'header',
330
+ metadata: {
331
+ __typename: 'GenAILatexItem',
332
+ latex_expression: '.',
333
+ font_height: 24,
334
+ padding: -5,
335
+ latex_image: {
336
+ __typename: 'GenAIMediaItem',
337
+ mime_type: image.mime_type || 'image/png',
338
+ url: image.url,
339
+ url_fallback: image.url,
340
+ width: image.width || 100,
341
+ height: image.height || 100,
342
+ expiration_timestamp_ms: Date.now() + 86400000
343
+ }
344
+ }
345
+ }]
346
+ });
347
+ }
348
+ sections.push({
349
+ view_model: {
350
+ primitives: [
351
+ { cta_text: text || '', cta_type: 'OPEN_URL', cta_url: url || '', __typename: 'GenAIFooterActionPrimitive' },
352
+ ...img
353
+ ],
354
+ __typename: 'GenAIActionRowLayoutViewModel'
355
+ }
356
+ });
357
+ }
358
+
359
+ return sendJsonMessage(jid, {
360
+ ...messageContextInfo,
361
+ richResponseMessage: {
362
+ unifiedResponse: { data: Buffer.from(JSON.stringify({ sections })).toString('base64') },
363
+ contextInfo: content?.contextInfo ?? {}
364
+ }
365
+ });
366
+ };
367
+
368
+ /**
369
+ * Sends a native quick-reply buttons message.
370
+ * buttons: [{ id: 'unique_id', text: 'Label' }]
371
+ */
372
+ const sendButtonsMessage = async (jid, { text = '', footer = '', buttons = [] } = {}, options = {}) => {
373
+ const interactiveMessage = {
374
+ body: { text },
375
+ ...(footer ? { footer: { text: footer } } : {}),
376
+ nativeFlowMessage: {
377
+ buttons: buttons.map(b => ({
378
+ name: b.name || 'quick_reply',
379
+ buttonParamsJson: JSON.stringify({ display_text: b.text, id: b.id })
380
+ }))
381
+ }
382
+ };
383
+ return sendJsonMessage(jid, { viewOnceMessage: { message: { interactiveMessage } } }, options);
384
+ };
385
+
386
+ /**
387
+ * Sends a native single-select list message.
388
+ * sections: [{ title: 'Section', rows: [{ title, description, id }] }]
389
+ */
390
+ const sendListMessage = async (jid, { text = '', footer = '', buttonText = 'Menu', sections = [] } = {}, options = {}) => {
391
+ const interactiveMessage = {
392
+ body: { text },
393
+ ...(footer ? { footer: { text: footer } } : {}),
394
+ nativeFlowMessage: {
395
+ buttons: [{
396
+ name: 'single_select',
397
+ buttonParamsJson: JSON.stringify({ title: buttonText, sections })
398
+ }]
399
+ }
400
+ };
401
+ return sendJsonMessage(jid, { viewOnceMessage: { message: { interactiveMessage } } }, options);
402
+ };
403
+
404
+ /**
405
+ * Sends a native horizontal-scroll carousel of cards (the correct native
406
+ * mechanism for side-by-side buttons — unlike the GenAI rich-response
407
+ * widget, which is designed as a vertical list).
408
+ * cards: [{ image: 'url or local path', mimetype, title, body, buttons: [{ id, text }] }]
409
+ */
410
+ const sendCarouselMessage = async (jid, { text = '', footer = '', cards = [] } = {}, options = {}) => {
411
+ const cardMessages = [];
412
+
413
+ for (const card of cards) {
414
+ let headerMedia = {};
415
+ if (card.image) {
416
+ const uploaded = await prepareWAMessageMedia(
417
+ { image: { url: card.image }, mimetype: card.mimetype || 'image/jpeg' },
418
+ { upload: waUploadToServer }
419
+ );
420
+ headerMedia = { imageMessage: uploaded.imageMessage };
421
+ }
422
+
423
+ cardMessages.push({
424
+ header: { title: card.title || '', hasMediaAttachment: !!card.image, ...headerMedia },
425
+ body: { text: card.body || '' },
426
+ nativeFlowMessage: {
427
+ buttons: (card.buttons || []).map(b => ({
428
+ name: b.name || 'quick_reply',
429
+ buttonParamsJson: JSON.stringify({ display_text: b.text, id: b.id })
430
+ }))
431
+ }
432
+ });
433
+ }
434
+
435
+ const message = {
436
+ viewOnceMessage: {
437
+ message: {
438
+ interactiveMessage: {
439
+ body: { text },
440
+ ...(footer ? { footer: { text: footer } } : {}),
441
+ carouselMessage: { cards: cardMessages, messageVersion: 1 }
442
+ }
443
+ }
444
+ }
445
+ };
446
+ return sendJsonMessage(jid, message, options);
447
+ };
448
+
449
+ /**
450
+ * Forwards an existing message (e.g. one pulled from the store) to
451
+ * another chat. Delegates to the library's own forward content builder
452
+ * (sendMessage's `forward` content type) so conversation→extendedText
453
+ * conversion, forwardingScore, and isForwarded are handled correctly
454
+ * instead of being reimplemented here.
455
+ */
456
+ const forwardMessage = async (jid, message, options = {}) => {
457
+ return sendMessage(jid, { forward: message, force: options.force }, options);
458
+ };
459
+
460
+ /**
461
+ * Builds a proper vCard string and sends it as a contact message.
462
+ * contact: { name, phone (E.164, no '+'), organization, waid }
463
+ * If multiple contacts are given, sends a contactsArrayMessage.
464
+ */
465
+ const sendVCard = async (jid, contacts, options = {}) => {
466
+ const list = Array.isArray(contacts) ? contacts : [contacts];
467
+ const built = list.map(c => {
468
+ const waid = c.waid || c.phone;
469
+ const vcard = 'BEGIN:VCARD\n'
470
+ + 'VERSION:3.0\n'
471
+ + `FN:${c.name || 'Unknown'}\n`
472
+ + (c.organization ? `ORG:${c.organization};\n` : '')
473
+ + `TEL;type=CELL;type=VOICE;waid=${waid}:+${c.phone}\n`
474
+ + 'END:VCARD';
475
+ return { displayName: c.name || 'Unknown', vcard };
476
+ });
477
+
478
+ if (built.length === 1) {
479
+ return sendMessage(jid, { contacts: { contacts: built } }, options);
480
+ }
481
+ return sendMessage(jid, { contacts: { displayName: `${built.length} contacts`, contacts: built } }, options);
482
+ };
483
+
484
+ /**
485
+ * Sends the same content to a list of jids sequentially, with an
486
+ * optional delay between sends (helps avoid rate limiting). Returns
487
+ * an array of { jid, ok, result|error } for each recipient.
488
+ * options.delayMs is stripped before being passed to sendMessage.
489
+ */
490
+ const broadcastMessage = async (jids, content, options = {}) => {
491
+ const { delayMs = 0, ...sendOptions } = options;
492
+ const results = [];
493
+ for (const jid of jids) {
494
+ try {
495
+ const result = await sendMessage(jid, content, sendOptions);
496
+ results.push({ jid, ok: true, result });
497
+ } catch (error) {
498
+ results.push({ jid, ok: false, error });
499
+ }
500
+ if (delayMs) await new Promise(resolve => setTimeout(resolve, delayMs));
501
+ }
502
+ return results;
503
+ };
504
+
505
+ return {
506
+ ...sock,
507
+ sendJsonMessage,
508
+ sendActionPoll,
509
+ resolvePollAction,
510
+ sendAlbumMessage,
511
+ sendStatusMention,
512
+ sendRichResponse,
513
+ sendButtonsMessage,
514
+ sendListMessage,
515
+ sendCarouselMessage,
516
+ sendHtml,
517
+ forwardMessage,
518
+ sendVCard,
519
+ broadcastMessage
520
+ };
521
+ };