@badzz88/baileys 8.4.7 → 8.5.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 (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1055 -337
  3. package/WAProto/index.js +5 -5
  4. package/lib/Defaults/index.js +187 -127
  5. package/lib/Defaults/phonenumber-mcc.json +223 -0
  6. package/lib/Signal/Group/ciphertext-message.js +14 -11
  7. package/lib/Signal/Group/group-session-builder.js +91 -29
  8. package/lib/Signal/Group/group_cipher.js +88 -81
  9. package/lib/Signal/Group/index.js +136 -12
  10. package/lib/Signal/Group/keyhelper.js +70 -15
  11. package/lib/Signal/Group/sender-chain-key.js +31 -25
  12. package/lib/Signal/Group/sender-key-distribution-message.js +65 -62
  13. package/lib/Signal/Group/sender-key-message.js +68 -65
  14. package/lib/Signal/Group/sender-key-name.js +43 -41
  15. package/lib/Signal/Group/sender-key-record.js +43 -40
  16. package/lib/Signal/Group/sender-key-state.js +96 -83
  17. package/lib/Signal/Group/sender-message-key.js +29 -25
  18. package/lib/Signal/libsignal.js +483 -424
  19. package/lib/Signal/lid-mapping.js +261 -276
  20. package/lib/Socket/Client/index.js +30 -3
  21. package/lib/Socket/Client/types.js +12 -10
  22. package/lib/Socket/Client/websocket.js +61 -53
  23. package/lib/Socket/aigroups.js +240 -0
  24. package/lib/Socket/business.js +422 -379
  25. package/lib/Socket/chats.js +1816 -1193
  26. package/lib/Socket/communities.js +549 -431
  27. package/lib/Socket/graphql.js +656 -0
  28. package/lib/Socket/groups.js +764 -374
  29. package/lib/Socket/index.js +38 -11
  30. package/lib/Socket/interactive-handler.js +522 -0
  31. package/lib/Socket/interop.js +549 -0
  32. package/lib/Socket/luxu.js +6 -5
  33. package/lib/Socket/managed-account.js +183 -0
  34. package/lib/Socket/messages-recv.js +2439 -1915
  35. package/lib/Socket/messages-send.js +1642 -1242
  36. package/lib/Socket/mex.js +59 -40
  37. package/lib/Socket/newsletter.js +812 -249
  38. package/lib/Socket/privacy.js +449 -0
  39. package/lib/Socket/registration.js +427 -0
  40. package/lib/Socket/socket.js +1077 -965
  41. package/lib/Socket/text-router.js +83 -0
  42. package/lib/Socket/username.js +237 -140
  43. package/lib/Store/index.js +36 -10
  44. package/lib/Store/make-cache-manager-store.js +85 -80
  45. package/lib/Store/make-in-memory-store.js +591 -185
  46. package/lib/Store/make-ordered-dictionary.js +78 -72
  47. package/lib/Store/object-repository.js +25 -28
  48. package/lib/Types/Auth.js +38 -2
  49. package/lib/Types/Bussines.js +2 -2
  50. package/lib/Types/Call.js +2 -2
  51. package/lib/Types/Chat.js +4 -8
  52. package/lib/Types/Contact.js +2 -2
  53. package/lib/Types/Events.js +2 -2
  54. package/lib/Types/GroupMetadata.js +2 -2
  55. package/lib/Types/Label.js +26 -24
  56. package/lib/Types/LabelAssociation.js +8 -6
  57. package/lib/Types/Message.js +95 -11
  58. package/lib/Types/Mex.js +112 -37
  59. package/lib/Types/Newsletter.js +121 -0
  60. package/lib/Types/Product.js +2 -2
  61. package/lib/Types/Signal.js +2 -2
  62. package/lib/Types/Socket.js +2 -3
  63. package/lib/Types/State.js +70 -56
  64. package/lib/Types/USync.js +2 -2
  65. package/lib/Types/index.js +55 -26
  66. package/lib/Utils/auth-utils.js +292 -288
  67. package/lib/Utils/browser-utils.js +113 -48
  68. package/lib/Utils/business.js +240 -224
  69. package/lib/Utils/chat-utils.js +1205 -869
  70. package/lib/Utils/command-loader.d.ts +31 -0
  71. package/lib/Utils/command-loader.js +100 -0
  72. package/lib/Utils/companion-reg-client-utils.js +41 -34
  73. package/lib/Utils/consumer-application.js +107 -0
  74. package/lib/Utils/crypto.js +144 -107
  75. package/lib/Utils/curve25519-js.js +275 -0
  76. package/lib/Utils/decode-wa-message.js +518 -308
  77. package/lib/Utils/event-buffer.js +596 -611
  78. package/lib/Utils/generics.js +514 -354
  79. package/lib/Utils/group-history.js +60 -0
  80. package/lib/Utils/history.js +244 -134
  81. package/lib/Utils/identity-change-handler.js +51 -49
  82. package/lib/Utils/index.js +57 -23
  83. package/lib/Utils/jid-display-normalization.js +218 -0
  84. package/lib/Utils/link-preview.js +135 -77
  85. package/lib/Utils/logger.js +9 -3
  86. package/lib/Utils/lt-hash.js +37 -6
  87. package/lib/Utils/make-mutex.js +36 -33
  88. package/lib/Utils/message-composer.js +439 -233
  89. package/lib/Utils/message-retry-manager.js +265 -260
  90. package/lib/Utils/messages-media.js +855 -747
  91. package/lib/Utils/messages.js +2528 -884
  92. package/lib/Utils/meta-ai-msmsg.js +262 -0
  93. package/lib/Utils/native-bridge.js +82 -0
  94. package/lib/Utils/noise-handler.js +195 -200
  95. package/lib/Utils/offline-node-processor.js +35 -33
  96. package/lib/Utils/pre-key-manager.js +103 -102
  97. package/lib/Utils/process-message.js +978 -560
  98. package/lib/Utils/reporting-utils.js +257 -253
  99. package/lib/Utils/session-pool.d.ts +16 -0
  100. package/lib/Utils/session-pool.js +94 -0
  101. package/lib/Utils/signal.js +218 -195
  102. package/lib/Utils/stanza-ack.js +31 -29
  103. package/lib/Utils/sticker.d.ts +17 -0
  104. package/lib/Utils/sticker.js +145 -0
  105. package/lib/Utils/sync-action-utils.js +50 -45
  106. package/lib/Utils/tc-token-utils.js +137 -139
  107. package/lib/Utils/use-multi-file-auth-state.js +109 -109
  108. package/lib/Utils/validate-connection.js +218 -202
  109. package/lib/Utils/view-once-cache.d.ts +9 -0
  110. package/lib/Utils/view-once-cache.js +79 -0
  111. package/lib/Utils/voip-rekey.js +22 -0
  112. package/lib/WABinary/constants.js +1302 -1299
  113. package/lib/WABinary/decode.js +343 -262
  114. package/lib/WABinary/encode.js +12 -4
  115. package/lib/WABinary/generic-utils.js +223 -187
  116. package/lib/WABinary/index.js +33 -6
  117. package/lib/WABinary/jid-utils.js +351 -88
  118. package/lib/WABinary/types.js +2 -2
  119. package/lib/WAM/BinaryInfo.js +12 -9
  120. package/lib/WAM/constants.js +39486 -22853
  121. package/lib/WAM/encode.js +132 -140
  122. package/lib/WAM/index.js +31 -4
  123. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +55 -0
  124. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +43 -0
  125. package/lib/WAUSync/Protocols/USyncContactProtocol.js +53 -51
  126. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +56 -53
  127. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +29 -26
  128. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +59 -0
  129. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +31 -0
  130. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +32 -0
  131. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +29 -0
  132. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +38 -37
  133. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +38 -0
  134. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +26 -24
  135. package/lib/WAUSync/Protocols/index.js +39 -6
  136. package/lib/WAUSync/USyncQuery.js +132 -97
  137. package/lib/WAUSync/USyncUser.js +49 -30
  138. package/lib/WAUSync/index.js +31 -4
  139. package/lib/antiban.js +4637 -0
  140. package/lib/index.js +48 -15
  141. package/package.json +55 -92
  142. package/lib/WAUSync/Protocols/USyncNewsletterProtocol.js +0 -263
  143. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +0 -51
  144. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +0 -29
@@ -1,1006 +1,1224 @@
1
- import NodeCache from '@cacheable/node-cache';
2
- import { Boom } from '@hapi/boom';
3
- import { proto } from '../../WAProto/index.js';
4
- import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
5
- import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateIOSMessageID, generateParticipantHashV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds, setBotMessageSecret } from '../Utils/index.js';
6
- import { getUrlInfo } from '../Utils/link-preview.js';
7
- import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
8
- import { getMessageReportingToken, shouldIncludeReportingToken } from '../Utils/reporting-utils.js';
9
- import { buildMergedTcTokenIndexWrite, isTcTokenExpired, resolveIssuanceJid, resolveTcTokenJid, shouldSendNewTcToken, storeTcTokensFromIqResult } from '../Utils/tc-token-utils.js';
10
- import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, isHostedLidUser, isHostedPnUser, isJidBot, isJidGroup, isJidMetaAI, isLidUser, isPnUser, jidDecode, jidEncode, jidNormalizedUser, PSA_WID, S_WHATSAPP_NET, getAdditionalNode, getBinaryNodeFilter, getBinaryFilteredBizBot, isInteropUser } from '../WABinary/index.js';
11
- import { USyncQuery, USyncUser } from '../WAUSync/index.js';
12
- import { makeUsernameSocket } from './username.js';
13
- import imup from './luxu.js';
14
- import * as Utils_1 from '../Utils/index.js';
15
- import { randomBytes } from 'crypto';
16
- export const makeMessagesSocket = (config) => {
17
- const { logger, linkPreviewImageThumbnailWidth, generateHighQualityLinkPreview, options: httpRequestOptions, patchMessageBeforeSending, cachedGroupMetadata, enableRecentMessageCache, maxMsgRetryCount, aiLabel } = config;
18
- const sock = makeUsernameSocket(config);
19
- const { ev, authState, messageMutex, signalRepository, upsertMessage, query, fetchPrivacySettings, sendNode, groupMetadata, groupToggleEphemeral, registerSocketEndHandler } = sock;
20
- const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
21
- /**
22
- * Set of tctoken storage JIDs with a fire-and-forget `issuePrivacyTokens` IQ in flight.
23
- * Prevents duplicate IQs from rapid back-to-back sends before `senderTimestamp` persists.
24
- * Entries are always removed in `.finally()`, so the set is bounded by concurrency.
25
- */
26
- const inFlightTcTokenIssuance = new Set();
27
- const userDevicesCache = config.userDevicesCache ||
28
- new NodeCache({
29
- stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
30
- useClones: false
31
- });
32
- /** Serializes writes to userDevicesCache across USync refresh and device-notification handling. */
33
- const devicesMutex = makeMutex();
34
- // Initialize message retry manager if enabled
35
- const messageRetryManager = enableRecentMessageCache ? new MessageRetryManager(logger, maxMsgRetryCount) : null;
36
- // Prevent race conditions in Signal session encryption by user
37
- const encryptionMutex = makeKeyedMutex();
38
- let mediaConn;
39
- /** Per-socket media host; updated whenever media_conn is fetched. Defaults to the public WhatsApp host. */
40
- let mediaHost = DEF_MEDIA_HOST;
41
- const refreshMediaConn = async (forceGet = false) => {
42
- const media = await mediaConn;
43
- if (!media || forceGet || new Date().getTime() - media.fetchDate.getTime() > media.ttl * 1000) {
44
- mediaConn = (async () => {
45
- const result = await query({
46
- tag: 'iq',
47
- attrs: {
48
- type: 'set',
49
- xmlns: 'w:m',
50
- to: S_WHATSAPP_NET
51
- },
52
- content: [{ tag: 'media_conn', attrs: {} }]
53
- });
54
- const mediaConnNode = getBinaryNodeChild(result, 'media_conn');
55
- // TODO: explore full length of data that whatsapp provides
56
- const node = {
57
- hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
58
- hostname: attrs.hostname,
59
- maxContentLengthBytes: +attrs.maxContentLengthBytes
60
- })),
61
- auth: mediaConnNode.attrs.auth,
62
- ttl: +mediaConnNode.attrs.ttl,
63
- fetchDate: new Date()
64
- };
65
- logger.debug('fetched media conn');
66
- if (node.hosts[0]) {
67
- mediaHost = node.hosts[0].hostname;
68
- }
69
- return node;
70
- })();
71
- }
72
- return mediaConn;
73
- };
74
- /**
75
- * generic send receipt function
76
- * used for receipts of phone call, read, delivery etc.
77
- * */
78
- const sendReceipt = async (jid, participant, messageIds, type) => {
79
- if (!messageIds || messageIds.length === 0) {
80
- throw new Boom('missing ids in receipt');
81
- }
82
- const node = {
83
- tag: 'receipt',
84
- attrs: {
85
- id: messageIds[0]
86
- }
87
- };
88
- const isReadReceipt = type === 'read' || type === 'read-self';
89
- if (isReadReceipt) {
90
- node.attrs.t = unixTimestampSeconds().toString();
91
- }
92
- if (type === 'sender' && (isPnUser(jid) || isLidUser(jid))) {
93
- node.attrs.recipient = jid;
94
- node.attrs.to = participant;
95
- }
96
- else {
97
- node.attrs.to = jid;
98
- if (participant) {
99
- node.attrs.participant = participant;
100
- }
101
- }
102
- if (type) {
103
- node.attrs.type = type;
104
- }
105
- const remainingMessageIds = messageIds.slice(1);
106
- if (remainingMessageIds.length) {
107
- node.content = [
108
- {
109
- tag: 'list',
110
- attrs: {},
111
- content: remainingMessageIds.map(id => ({
112
- tag: 'item',
113
- attrs: { id }
114
- }))
115
- }
116
- ];
117
- }
118
- logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt for messages');
119
- await sendNode(node);
120
- };
121
- /** Correctly bulk send receipts to multiple chats, participants */
122
- const sendReceipts = async (keys, type) => {
123
- const recps = aggregateMessageKeysNotFromMe(keys);
124
- for (const { jid, participant, messageIds } of recps) {
125
- await sendReceipt(jid, participant, messageIds, type);
126
- }
127
- };
128
- /** Bulk read messages. Keys can be from different chats & participants */
129
- const readMessages = async (keys) => {
130
- const privacySettings = await fetchPrivacySettings();
131
- // based on privacy settings, we have to change the read type
132
- const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self';
133
- await sendReceipts(keys, readType);
134
- };
135
- /** Fetch all the devices we've to send a message to */
136
- const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
137
- const deviceResults = [];
138
- if (!useCache) {
139
- logger.debug('not using cache for devices');
140
- }
141
- const toFetch = [];
142
- const jidsWithUser = jids
143
- .map(jid => {
144
- const decoded = jidDecode(jid);
145
- const user = decoded?.user;
146
- const device = decoded?.device;
147
- const isExplicitDevice = typeof device === 'number' && device >= 0;
148
- if (isExplicitDevice && user) {
149
- deviceResults.push({
150
- user,
151
- device,
152
- jid
153
- });
154
- return null;
155
- }
156
- jid = jidNormalizedUser(jid);
157
- return { jid, user };
158
- })
159
- .filter(jid => jid !== null);
160
- let mgetDevices;
161
- if (useCache && userDevicesCache.mget) {
162
- const usersToFetch = jidsWithUser.map(j => j?.user).filter(Boolean);
163
- mgetDevices = await userDevicesCache.mget(usersToFetch);
164
- }
165
- for (const { jid, user } of jidsWithUser) {
166
- if (useCache) {
167
- const devices = mgetDevices?.[user] ||
168
- (userDevicesCache.mget ? undefined : (await userDevicesCache.get(user)));
169
- if (devices) {
170
- const devicesWithJid = devices.map(d => ({
171
- ...d,
172
- jid: jidEncode(d.user, d.server, d.device)
173
- }));
174
- deviceResults.push(...devicesWithJid);
175
- logger.trace({ user }, 'using cache for devices');
176
- }
177
- else {
178
- toFetch.push(jid);
179
- }
180
- }
181
- else {
182
- toFetch.push(jid);
183
- }
184
- }
185
- if (!toFetch.length) {
186
- return deviceResults;
187
- }
188
- const requestedLidUsers = new Set();
189
- for (const jid of toFetch) {
190
- if (isLidUser(jid) || isHostedLidUser(jid)) {
191
- const user = jidDecode(jid)?.user;
192
- if (user)
193
- requestedLidUsers.add(user);
194
- }
195
- }
196
- const query = new USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol();
197
- for (const jid of toFetch) {
198
- query.withUser(new USyncUser().withId(jid)); // todo: investigate - the idea here is that <user> should have an inline lid field with the lid being the pn equivalent
199
- }
200
- const result = await sock.executeUSyncQuery(query);
201
- if (result) {
202
- // TODO: LID MAP this stuff (lid protocol will now return lid with devices)
203
- const lidResults = result.list.filter(a => !!a.lid);
204
- if (lidResults.length > 0) {
205
- logger.trace('Storing LID maps from device call');
206
- await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })));
207
- // Force-refresh sessions for newly mapped LIDs to align identity addressing
208
- try {
209
- const lids = lidResults.map(a => a.lid);
210
- if (lids.length) {
211
- await assertSessions(lids, true);
212
- }
213
- }
214
- catch (e) {
215
- logger.warn({ e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs');
216
- }
217
- }
218
- const extracted = extractDeviceJids(result?.list, authState.creds.me.id, authState.creds.me.lid, ignoreZeroDevices);
219
- const deviceMap = {};
220
- for (const item of extracted) {
221
- deviceMap[item.user] = deviceMap[item.user] || [];
222
- deviceMap[item.user]?.push(item);
223
- }
224
- // Process each user's devices as a group for bulk LID migration
225
- for (const [user, userDevices] of Object.entries(deviceMap)) {
226
- const isLidUser = requestedLidUsers.has(user);
227
- // Process all devices for this user
228
- for (const item of userDevices) {
229
- const finalJid = isLidUser
230
- ? jidEncode(user, item.server, item.device)
231
- : jidEncode(item.user, item.server, item.device);
232
- deviceResults.push({
233
- ...item,
234
- jid: finalJid
235
- });
236
- logger.debug({
237
- user: item.user,
238
- device: item.device,
239
- finalJid,
240
- usedLid: isLidUser
241
- }, 'Processed device with LID priority');
242
- }
243
- }
244
- await devicesMutex.mutex(async () => {
245
- if (userDevicesCache.mset) {
246
- // if the cache supports mset, we can set all devices in one go
247
- await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })));
248
- }
249
- else {
250
- for (const key in deviceMap) {
251
- if (deviceMap[key])
252
- await userDevicesCache.set(key, deviceMap[key]);
253
- }
254
- }
255
- });
256
- const userDeviceUpdates = {};
257
- for (const [userId, devices] of Object.entries(deviceMap)) {
258
- if (devices && devices.length > 0) {
259
- userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0');
260
- }
261
- }
262
- if (Object.keys(userDeviceUpdates).length > 0) {
263
- try {
264
- await authState.keys.set({ 'device-list': userDeviceUpdates });
265
- logger.debug({ userCount: Object.keys(userDeviceUpdates).length }, 'stored user device lists for bulk migration');
266
- }
267
- catch (error) {
268
- logger.warn({ error }, 'failed to store user device lists');
269
- }
270
- }
271
- }
272
- return deviceResults;
273
- };
274
- /**
275
- * Update Member Label
276
- */
277
- const updateMemberLabel = (jid, memberLabel) => {
278
- return relayMessage(jid, {
279
- protocolMessage: {
280
- type: proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE,
281
- memberLabel: {
282
- label: memberLabel?.slice(0, 30),
283
- labelTimestamp: unixTimestampSeconds()
284
- }
285
- }
286
- }, {
287
- additionalNodes: [
288
- {
289
- tag: 'meta',
290
- attrs: {
291
- tag_reason: 'user_update',
292
- appdata: 'member_tag'
293
- },
294
- content: undefined
295
- }
296
- ]
297
- });
298
- };
299
- const assertSessions = async (jids, force) => {
300
- let didFetchNewSession = false;
301
- const uniqueJids = [...new Set(jids)];
302
- const jidsRequiringFetch = [];
303
- logger.debug({ jids }, 'assertSessions call with jids');
304
- for (const jid of uniqueJids) {
305
- if (!force) {
306
- const sessionValidation = await signalRepository.validateSession(jid);
307
- if (sessionValidation.exists) {
308
- continue;
309
- }
310
- }
311
- jidsRequiringFetch.push(jid);
312
- }
313
- if (jidsRequiringFetch.length) {
314
- // LID if mapped, otherwise original
315
- const wireJids = [
316
- ...jidsRequiringFetch.filter(jid => !!isLidUser(jid) || !!isHostedLidUser(jid)),
317
- ...((await signalRepository.lidMapping.getLIDsForPNs(jidsRequiringFetch.filter(jid => !!isPnUser(jid) || !!isHostedPnUser(jid)))) || []).map(a => a.lid)
318
- ];
319
- logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions');
320
- const result = await query({
321
- tag: 'iq',
322
- attrs: {
323
- xmlns: 'encrypt',
324
- type: 'get',
325
- to: S_WHATSAPP_NET
326
- },
327
- content: [
328
- {
329
- tag: 'key',
330
- attrs: {},
331
- content: wireJids.map(jid => {
332
- const attrs = { jid };
333
- if (force)
334
- attrs.reason = 'identity';
335
- return { tag: 'user', attrs };
336
- })
337
- }
338
- ]
339
- });
340
- await parseAndInjectE2ESessions(result, signalRepository);
341
- didFetchNewSession = true;
342
- }
343
- return didFetchNewSession;
344
- };
345
- const sendPeerDataOperationMessage = async (pdoMessage) => {
346
- //TODO: for later, abstract the logic to send a Peer Message instead of just PDO - useful for App State Key Resync with phone
347
- if (!authState.creds.me?.id) {
348
- throw new Boom('Not authenticated');
349
- }
350
- const protocolMessage = {
351
- protocolMessage: {
352
- peerDataOperationRequestMessage: pdoMessage,
353
- type: proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE
354
- }
355
- };
356
- const meJid = jidNormalizedUser(authState.creds.me.id);
357
- const msgId = await relayMessage(meJid, protocolMessage, {
358
- additionalAttributes: {
359
- category: 'peer',
360
- push_priority: 'high_force'
361
- },
362
- additionalNodes: [
363
- {
364
- tag: 'meta',
365
- attrs: { appdata: 'default' }
366
- }
367
- ]
368
- });
369
- return msgId;
370
- };
371
- const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
372
- if (!recipientJids.length) {
373
- return { nodes: [], shouldIncludeDeviceIdentity: false };
374
- }
375
- const patched = await patchMessageBeforeSending(message, recipientJids);
376
- const patchedMessages = Array.isArray(patched)
377
- ? patched
378
- : recipientJids.map(jid => ({ recipientJid: jid, message: patched }));
379
- let shouldIncludeDeviceIdentity = false;
380
- const meId = authState.creds.me.id;
381
- const meLid = authState.creds.me?.lid;
382
- const meLidUser = meLid ? jidDecode(meLid)?.user : null;
383
- const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
384
- try {
385
- if (!jid)
386
- return null;
387
- let msgToEncrypt = patchedMessage;
388
- if (dsmMessage) {
389
- const { user: targetUser } = jidDecode(jid);
390
- const { user: ownPnUser } = jidDecode(meId);
391
- const ownLidUser = meLidUser;
392
- const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser);
393
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid);
394
- if (isOwnUser && !isExactSenderDevice) {
395
- msgToEncrypt = dsmMessage;
396
- logger.debug({ jid, targetUser }, 'Using DSM for own device');
397
- }
398
- }
399
- const bytes = encodeWAMessage(msgToEncrypt);
400
- const mutexKey = jid;
401
- const node = await encryptionMutex.mutex(mutexKey, async () => {
402
- const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes });
403
- if (type === 'pkmsg') {
404
- shouldIncludeDeviceIdentity = true;
405
- }
406
- return {
407
- tag: 'to',
408
- attrs: { jid },
409
- content: [
410
- {
411
- tag: 'enc',
412
- attrs: { v: '2', type, ...(extraAttrs || {}) },
413
- content: ciphertext
414
- }
415
- ]
416
- };
417
- });
418
- return node;
419
- }
420
- catch (err) {
421
- logger.error({ jid, err }, 'Failed to encrypt for recipient');
422
- return null;
423
- }
424
- });
425
- const nodes = (await Promise.all(encryptionPromises)).filter(node => node !== null);
426
- if (recipientJids.length > 0 && nodes.length === 0) {
427
- throw new Boom('All encryptions failed', { statusCode: 500 });
428
- }
429
- return { nodes, shouldIncludeDeviceIdentity };
430
- };
431
- const relayMessage = async (
432
- jid,
433
- message,
434
- {
435
- messageId: msgId,
436
- participant = false,
437
- noSelfSync = false,
438
- additionalAttributes,
439
- additionalNodes,
440
- useUserDevicesCache,
441
- useCachedGroupMetadata,
442
- statusJidList
443
- }
1
+ 'use strict'
2
+ var __importDefault =
3
+ (this && this.__importDefault) ||
4
+ function (mod) {
5
+ return mod && mod.__esModule ? mod : { default: mod }
6
+ }
7
+ Object.defineProperty(exports, '__esModule', { value: true })
8
+ exports.makeMessagesSocket = void 0
9
+ const node_cache_1 = __importDefault(require('@cacheable/node-cache'))
10
+ const boom_1 = require('@hapi/boom')
11
+ const index_js_1 = require('../../WAProto/index.js')
12
+ const Defaults_1 = require('../Defaults')
13
+ const Utils_1 = require('../Utils')
14
+ const link_preview_1 = require('../Utils/link-preview')
15
+ const make_mutex_1 = require('../Utils/make-mutex')
16
+ const reporting_utils_1 = require('../Utils/reporting-utils')
17
+ const tc_token_utils_1 = require('../Utils/tc-token-utils')
18
+ const jid_display_normalization_1 = require('../Utils/jid-display-normalization')
19
+ const WABinary_1 = require('../WABinary')
20
+ const WAUSync_1 = require('../WAUSync')
21
+ const message_composer_1 = require('../Utils/message-composer.js')
22
+ const interactive_handler_1 = require('./interactive-handler.js')
23
+ const username_1 = require('./username')
24
+ const { setBotMessageSecret } = require('../Utils/decode-wa-message')
25
+ const makeMessagesSocket = config => {
26
+ const {
27
+ logger,
28
+ linkPreviewImageThumbnailWidth,
29
+ generateHighQualityLinkPreview,
30
+ options: httpRequestOptions,
31
+ patchMessageBeforeSending,
32
+ cachedGroupMetadata,
33
+ enableRecentMessageCache,
34
+ maxMsgRetryCount
35
+ } = config
36
+ const sock = (0, username_1.makeUsernameSocket)(config)
37
+ const {
38
+ ev,
39
+ authState,
40
+ messageMutex,
41
+ signalRepository,
42
+ upsertMessage,
43
+ query,
44
+ fetchPrivacySettings,
45
+ sendNode,
46
+ groupMetadata,
47
+ groupToggleEphemeral,
48
+ trustInteropContact
49
+ } = sock
50
+ const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
51
+ /**
52
+ * Set of tctoken storage JIDs with a fire-and-forget issuePrivacyTokens IQ in flight.
53
+ * Prevents duplicate IQs from rapid back-to-back sends before senderTimestamp persists.
54
+ */
55
+ const inFlightTcTokenIssuance = new Set()
56
+ const userDevicesCache =
57
+ config.userDevicesCache ||
58
+ new node_cache_1.default({
59
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
60
+ useClones: false
61
+ })
62
+ const peerSessionsCache = new node_cache_1.default({
63
+ stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.USER_DEVICES,
64
+ useClones: false
65
+ })
66
+ // Initialize message retry manager if enabled
67
+ const messageRetryManager = enableRecentMessageCache
68
+ ? new Utils_1.MessageRetryManager(logger, maxMsgRetryCount)
69
+ : null
70
+ // Prevent race conditions in Signal session encryption by user
71
+ const encryptionMutex = (0, make_mutex_1.makeKeyedMutex)()
72
+ let mediaConn
73
+ const refreshMediaConn = async (forceGet = false) => {
74
+ const media = await mediaConn
75
+ if (!media || forceGet || new Date().getTime() - media.fetchDate.getTime() > media.ttl * 1000) {
76
+ mediaConn = (async () => {
77
+ const result = await query({
78
+ tag: 'iq',
79
+ attrs: {
80
+ type: 'set',
81
+ xmlns: 'w:m',
82
+ to: WABinary_1.S_WHATSAPP_NET
83
+ },
84
+ content: [{ tag: 'media_conn', attrs: {} }]
85
+ })
86
+ const mediaConnNode = (0, WABinary_1.getBinaryNodeChild)(result, 'media_conn')
87
+ // TODO: explore full length of data that whatsapp provides
88
+ const node = {
89
+ hosts: (0, WABinary_1.getBinaryNodeChildren)(mediaConnNode, 'host').map(({ attrs }) => ({
90
+ hostname: attrs.hostname,
91
+ maxContentLengthBytes: +attrs.maxContentLengthBytes
92
+ })),
93
+ auth: mediaConnNode.attrs.auth,
94
+ ttl: +mediaConnNode.attrs.ttl,
95
+ fetchDate: new Date()
96
+ }
97
+ logger.debug('fetched media conn')
98
+ return node
99
+ })()
100
+ }
101
+ return mediaConn
102
+ }
103
+ /**
104
+ * generic send receipt function
105
+ * used for receipts of phone call, read, delivery etc.
106
+ * */
107
+ const sendReceipt = async (jid, participant, messageIds, type, sts) => {
108
+ if (!messageIds || messageIds.length === 0) {
109
+ throw new boom_1.Boom('missing ids in receipt')
110
+ }
111
+ const isInterop = (0, WABinary_1.isInteropUser)(jid)
112
+ const node = {
113
+ tag: 'receipt',
114
+ attrs: {
115
+ id: messageIds[0]
116
+ }
117
+ }
118
+ const isReadReceipt = type === 'read' || type === 'read-self'
119
+ if (isReadReceipt) {
120
+ node.attrs.t = (0, Utils_1.unixTimestampSeconds)().toString()
121
+ }
122
+ if (type === 'sender' && ((0, WABinary_1.isPnUser)(jid) || (0, WABinary_1.isLidUser)(jid))) {
123
+ node.attrs.recipient = jid
124
+ node.attrs.to = participant
125
+ } else if (isInterop && !type) {
126
+ // Delivery receipt to interop contact: WA sends to device-qualified JID (user:0@interop)
127
+ const { user } = (0, WABinary_1.jidDecode)(jid)
128
+ node.attrs.to = `${user}:0@interop`
129
+ } else {
130
+ node.attrs.to = jid
131
+ if (participant) {
132
+ node.attrs.participant = participant
133
+ }
134
+ }
135
+ if (type) {
136
+ node.attrs.type = type
137
+ }
138
+ // Interop bridge requires sts (microsecond stanza timestamp) from the original message
139
+ if (isInterop && sts) {
140
+ node.attrs.sts = sts
141
+ }
142
+ const remainingMessageIds = messageIds.slice(1)
143
+ if (remainingMessageIds.length) {
144
+ node.content = [
145
+ {
146
+ tag: 'list',
147
+ attrs: {},
148
+ content: remainingMessageIds.map(id => ({
149
+ tag: 'item',
150
+ attrs: { id }
151
+ }))
152
+ }
153
+ ]
154
+ }
155
+ logger.debug({ attrs: node.attrs, messageIds }, 'sending receipt for messages')
156
+ await sendNode(node)
157
+ }
158
+ /** Correctly bulk send receipts to multiple chats, participants */
159
+ const sendReceipts = async (keys, type) => {
160
+ const recps = (0, Utils_1.aggregateMessageKeysNotFromMe)(keys)
161
+ for (const { jid, participant, messageIds, sts } of recps) {
162
+ await sendReceipt(jid, participant, messageIds, type, sts)
163
+ }
164
+ }
165
+ /** Bulk read messages. Keys can be from different chats & participants */
166
+ const readMessages = async keys => {
167
+ const privacySettings = await fetchPrivacySettings()
168
+ // based on privacy settings, we have to change the read type
169
+ const readType = privacySettings.readreceipts === 'all' ? 'read' : 'read-self'
170
+ await sendReceipts(keys, readType)
171
+ }
172
+ /** Fetch all the devices we've to send a message to */
173
+ const getUSyncDevices = async (jids, useCache, ignoreZeroDevices) => {
174
+ const deviceResults = []
175
+ if (!useCache) {
176
+ logger.debug('not using cache for devices')
177
+ }
178
+ const toFetch = []
179
+ const jidsWithUser = jids
180
+ .map(jid => {
181
+ const decoded = (0, WABinary_1.jidDecode)(jid)
182
+ const user = decoded?.user
183
+ const device = decoded?.device
184
+ const isExplicitDevice = typeof device === 'number' && device >= 0
185
+ if (isExplicitDevice && user) {
186
+ deviceResults.push({
187
+ user,
188
+ device,
189
+ jid
190
+ })
191
+ return null
192
+ }
193
+ jid = (0, WABinary_1.jidNormalizedUser)(jid)
194
+ return { jid, user }
195
+ })
196
+ .filter(jid => jid !== null)
197
+ let mgetDevices
198
+ if (useCache && userDevicesCache.mget) {
199
+ const usersToFetch = jidsWithUser.map(j => j?.user).filter(Boolean)
200
+ mgetDevices = await userDevicesCache.mget(usersToFetch)
201
+ }
202
+ for (const { jid, user } of jidsWithUser) {
203
+ if (useCache) {
204
+ const devices = mgetDevices?.[user] || (userDevicesCache.mget ? undefined : await userDevicesCache.get(user))
205
+ if (devices) {
206
+ const devicesWithJid = devices.map(d => ({
207
+ ...d,
208
+ jid: (0, WABinary_1.jidEncode)(d.user, d.server, d.device)
209
+ }))
210
+ deviceResults.push(...devicesWithJid)
211
+ logger.trace({ user }, 'using cache for devices')
212
+ } else {
213
+ toFetch.push(jid)
214
+ }
215
+ } else {
216
+ toFetch.push(jid)
217
+ }
218
+ }
219
+ if (!toFetch.length) {
220
+ return deviceResults
221
+ }
222
+ const requestedLidUsers = new Set()
223
+ for (const jid of toFetch) {
224
+ if ((0, WABinary_1.isLidUser)(jid) || (0, WABinary_1.isHostedLidUser)(jid)) {
225
+ const user = (0, WABinary_1.jidDecode)(jid)?.user
226
+ if (user) requestedLidUsers.add(user)
227
+ }
228
+ }
229
+ const query = new WAUSync_1.USyncQuery().withContext('message').withDeviceProtocol().withLIDProtocol()
230
+ for (const jid of toFetch) {
231
+ query.withUser(new WAUSync_1.USyncUser().withId(jid)) // todo: investigate - the idea here is that <user> should have an inline lid field with the lid being the pn equivalent
232
+ }
233
+ const result = await sock.executeUSyncQuery(query)
234
+ if (result) {
235
+ // TODO: LID MAP this stuff (lid protocol will now return lid with devices)
236
+ const lidResults = result.list.filter(a => !!a.lid)
237
+ if (lidResults.length > 0) {
238
+ logger.trace('Storing LID maps from device call')
239
+ await signalRepository.lidMapping.storeLIDPNMappings(lidResults.map(a => ({ lid: a.lid, pn: a.id })))
240
+ // Force-refresh sessions for newly mapped LIDs to align identity addressing
241
+ try {
242
+ const lids = lidResults.map(a => a.lid)
243
+ if (lids.length) {
244
+ await assertSessions(lids, true)
245
+ }
246
+ } catch (e) {
247
+ logger.warn({ e, count: lidResults.length }, 'failed to assert sessions for newly mapped LIDs')
248
+ }
249
+ }
250
+ const extracted = (0, Utils_1.extractDeviceJids)(
251
+ result?.list,
252
+ authState.creds.me.id,
253
+ authState.creds.me.lid,
254
+ ignoreZeroDevices
255
+ )
256
+ const deviceMap = {}
257
+ for (const item of extracted) {
258
+ deviceMap[item.user] = deviceMap[item.user] || []
259
+ deviceMap[item.user]?.push(item)
260
+ }
261
+ // Process each user's devices as a group for bulk LID migration
262
+ for (const [user, userDevices] of Object.entries(deviceMap)) {
263
+ const isLidUser = requestedLidUsers.has(user)
264
+ // Process all devices for this user
265
+ for (const item of userDevices) {
266
+ const finalJid = isLidUser
267
+ ? (0, WABinary_1.jidEncode)(user, item.server, item.device)
268
+ : (0, WABinary_1.jidEncode)(item.user, item.server, item.device)
269
+ deviceResults.push({
270
+ ...item,
271
+ jid: finalJid
272
+ })
273
+ logger.debug(
274
+ {
275
+ user: item.user,
276
+ device: item.device,
277
+ finalJid,
278
+ usedLid: isLidUser
279
+ },
280
+ 'Processed device with LID priority'
281
+ )
282
+ }
283
+ }
284
+ if (userDevicesCache.mset) {
285
+ // if the cache supports mset, we can set all devices in one go
286
+ await userDevicesCache.mset(Object.entries(deviceMap).map(([key, value]) => ({ key, value })))
287
+ } else {
288
+ for (const key in deviceMap) {
289
+ if (deviceMap[key]) await userDevicesCache.set(key, deviceMap[key])
290
+ }
291
+ }
292
+ const userDeviceUpdates = {}
293
+ for (const [userId, devices] of Object.entries(deviceMap)) {
294
+ if (devices && devices.length > 0) {
295
+ userDeviceUpdates[userId] = devices.map(d => d.device?.toString() || '0')
296
+ }
297
+ }
298
+ if (Object.keys(userDeviceUpdates).length > 0) {
299
+ try {
300
+ await authState.keys.set({ 'device-list': userDeviceUpdates })
301
+ logger.debug(
302
+ { userCount: Object.keys(userDeviceUpdates).length },
303
+ 'stored user device lists for bulk migration'
304
+ )
305
+ } catch (error) {
306
+ logger.warn({ error }, 'failed to store user device lists')
307
+ }
308
+ }
309
+ }
310
+ return deviceResults
311
+ }
312
+ /**
313
+ * Update Member Label
314
+ */
315
+ const updateMemberLabel = (jid, memberLabel) => {
316
+ return relayMessage(
317
+ jid,
318
+ {
319
+ protocolMessage: {
320
+ type: index_js_1.proto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE,
321
+ memberLabel: {
322
+ label: memberLabel?.slice(0, 30),
323
+ labelTimestamp: (0, Utils_1.unixTimestampSeconds)()
324
+ }
325
+ }
326
+ },
327
+ {
328
+ additionalNodes: [
329
+ {
330
+ tag: 'meta',
331
+ attrs: {
332
+ tag_reason: 'user_update',
333
+ appdata: 'member_tag'
334
+ },
335
+ content: undefined
336
+ }
337
+ ]
338
+ }
339
+ )
340
+ }
341
+ const assertSessions = async (jids, force) => {
342
+ let didFetchNewSession = false
343
+ const uniqueJids = [...new Set(jids)] // Deduplicate JIDs
344
+ const jidsRequiringFetch = []
345
+ logger.debug({ jids }, 'assertSessions call with jids')
346
+ // Check peerSessionsCache and validate sessions using libsignal loadSession
347
+ for (const jid of uniqueJids) {
348
+ const signalId = signalRepository.jidToSignalProtocolAddress(jid)
349
+ const cachedSession = peerSessionsCache.get(signalId)
350
+ if (cachedSession !== undefined) {
351
+ if (cachedSession && !force) {
352
+ continue // Session exists in cache
353
+ }
354
+ } else {
355
+ const sessionValidation = await signalRepository.validateSession(jid)
356
+ const hasSession = sessionValidation.exists
357
+ peerSessionsCache.set(signalId, hasSession)
358
+ if (hasSession && !force) {
359
+ continue
360
+ }
361
+ }
362
+ jidsRequiringFetch.push(jid)
363
+ }
364
+ if (jidsRequiringFetch.length) {
365
+ // LID if mapped, otherwise original; interop JIDs pass through as-is
366
+ const wireJids = [
367
+ ...jidsRequiringFetch.filter(jid => !!(0, WABinary_1.isLidUser)(jid) || !!(0, WABinary_1.isHostedLidUser)(jid)),
368
+ ...(
369
+ (await signalRepository.lidMapping.getLIDsForPNs(
370
+ jidsRequiringFetch.filter(jid => !!(0, WABinary_1.isPnUser)(jid) || !!(0, WABinary_1.isHostedPnUser)(jid))
371
+ )) || []
372
+ ).map(a => a.lid),
373
+ ...jidsRequiringFetch.filter(jid => (0, WABinary_1.isInteropUser)(jid))
374
+ ]
375
+ const interopFetches = wireJids.filter(j => (0, WABinary_1.isInteropUser)(j))
376
+ if (interopFetches.length) {
377
+ logger.info({ interopFetches }, '[interop] fetching pre-key bundle for interop device(s)')
378
+ }
379
+ logger.debug({ jidsRequiringFetch, wireJids }, 'fetching sessions')
380
+ if (!wireJids.length) {
381
+ logger.debug({ jidsRequiringFetch }, 'assertSessions: no wire JIDs to fetch (all unsupported domain)')
382
+ return didFetchNewSession
383
+ }
384
+ const result = await query({
385
+ tag: 'iq',
386
+ attrs: {
387
+ xmlns: 'encrypt',
388
+ type: 'get',
389
+ to: WABinary_1.S_WHATSAPP_NET
390
+ },
391
+ content: [
392
+ {
393
+ tag: 'key',
394
+ attrs: {},
395
+ content: wireJids.map(jid => {
396
+ const attrs = { jid }
397
+ if (force) attrs.reason = 'identity'
398
+ return { tag: 'user', attrs }
399
+ })
400
+ }
401
+ ]
402
+ })
403
+ if (interopFetches.length) {
404
+ logger.debug({ interopFetches }, '[interop] pre-key IQ response received')
405
+ }
406
+ await (0, Utils_1.parseAndInjectE2ESessions)(result, signalRepository)
407
+ didFetchNewSession = true
408
+ // Cache fetched sessions using wire JIDs
409
+ for (const wireJid of wireJids) {
410
+ const signalId = signalRepository.jidToSignalProtocolAddress(wireJid)
411
+ peerSessionsCache.set(signalId, true)
412
+ }
413
+ }
414
+ return didFetchNewSession
415
+ }
416
+ const sendPeerDataOperationMessage = async pdoMessage => {
417
+ //TODO: for later, abstract the logic to send a Peer Message instead of just PDO - useful for App State Key Resync with phone
418
+ if (!authState.creds.me?.id) {
419
+ throw new boom_1.Boom('Not authenticated')
420
+ }
421
+ const protocolMessage = {
422
+ protocolMessage: {
423
+ peerDataOperationRequestMessage: pdoMessage,
424
+ type: index_js_1.proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE
425
+ }
426
+ }
427
+ const meJid = (0, WABinary_1.jidNormalizedUser)(authState.creds.me.id)
428
+ const msgId = await relayMessage(meJid, protocolMessage, {
429
+ additionalAttributes: {
430
+ category: 'peer',
431
+ push_priority: 'high_force'
432
+ },
433
+ additionalNodes: [
434
+ {
435
+ tag: 'meta',
436
+ attrs: { appdata: 'default' }
437
+ }
438
+ ]
439
+ })
440
+ return msgId
441
+ }
442
+ const createParticipantNodes = async (recipientJids, message, extraAttrs, dsmMessage) => {
443
+ if (!recipientJids.length) {
444
+ return { nodes: [], shouldIncludeDeviceIdentity: false }
445
+ }
446
+ const patched = await patchMessageBeforeSending(message, recipientJids)
447
+ const patchedMessages = Array.isArray(patched)
448
+ ? patched
449
+ : recipientJids.map(jid => ({ recipientJid: jid, message: patched }))
450
+ let shouldIncludeDeviceIdentity = false
451
+ const meId = authState.creds.me.id
452
+ const meLid = authState.creds.me?.lid
453
+ const meLidUser = meLid ? (0, WABinary_1.jidDecode)(meLid)?.user : null
454
+ const encryptionPromises = patchedMessages.map(async ({ recipientJid: jid, message: patchedMessage }) => {
455
+ try {
456
+ if (!jid) return null
457
+ let msgToEncrypt = patchedMessage
458
+ if (dsmMessage) {
459
+ const { user: targetUser } = (0, WABinary_1.jidDecode)(jid)
460
+ const { user: ownPnUser } = (0, WABinary_1.jidDecode)(meId)
461
+ const ownLidUser = meLidUser
462
+ const isOwnUser = targetUser === ownPnUser || (ownLidUser && targetUser === ownLidUser)
463
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
464
+ if (isOwnUser && !isExactSenderDevice) {
465
+ msgToEncrypt = dsmMessage
466
+ logger.debug({ jid, targetUser }, 'Using DSM for own device')
467
+ }
468
+ }
469
+ const bytes = (0, Utils_1.encodeWAMessage)(msgToEncrypt)
470
+ const mutexKey = jid
471
+ const node = await encryptionMutex.mutex(mutexKey, async () => {
472
+ const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
473
+ if (type === 'pkmsg') {
474
+ shouldIncludeDeviceIdentity = true
475
+ }
476
+ return {
477
+ tag: 'to',
478
+ attrs: { jid },
479
+ content: [
480
+ {
481
+ tag: 'enc',
482
+ attrs: { v: '2', type, ...(extraAttrs || {}) },
483
+ content: ciphertext
484
+ }
485
+ ]
486
+ }
487
+ })
488
+ return node
489
+ } catch (err) {
490
+ logger.error({ jid, err }, 'Failed to encrypt for recipient')
491
+ return null
492
+ }
493
+ })
494
+ const nodes = (await Promise.all(encryptionPromises)).filter(node => node !== null)
495
+ if (recipientJids.length > 0 && nodes.length === 0) {
496
+ throw new boom_1.Boom('All encryptions failed', { statusCode: 500 })
497
+ }
498
+ return { nodes, shouldIncludeDeviceIdentity }
499
+ }
500
+ const relayMessage = async (
501
+ jid,
502
+ message,
503
+ {
504
+ messageId: msgId,
505
+ participant,
506
+ additionalAttributes,
507
+ additionalNodes,
508
+ useUserDevicesCache,
509
+ useCachedGroupMetadata,
510
+ statusJidList
511
+ }
444
512
  ) => {
445
- const meId = authState.creds.me.id
446
- const meLid = authState.creds.me?.lid
447
- const isRetryResend = Boolean(participant?.jid)
448
- let shouldIncludeDeviceIdentity = isRetryResend
449
- const statusJid = 'status@broadcast'
450
- const { user, server } = jidDecode(jid)
451
- const isGroup = server === 'g.us'
452
- const isStatus = jid === statusJid
453
- const isLid = server === 'lid'
454
- const isNewsletter = server === 'newsletter'
455
- const isInterop = isInteropUser(jid)
456
- const isGroupOrStatus = isGroup || isStatus
457
- const finalJid = jid
458
- const iosBros = config.browser[0] === "iOS" || config.browser[1] === "Safari";
459
- msgId = iosBros ? generateIOSMessageID() : msgId ?? generateMessageIDV2(meId)
460
- useUserDevicesCache = useUserDevicesCache!== false
461
- useCachedGroupMetadata = useCachedGroupMetadata!== false &&!isStatus
462
- const participants = []
463
- const destinationJid =!isStatus? finalJid : statusJid
464
- const binaryNodeContent = []
465
- const devices = []
466
- let reportingMessage
467
- const meMsg = {
468
- deviceSentMessage: { destinationJid, message },
469
- messageContextInfo: message.messageContextInfo
470
- }
471
- const extraAttrs = {}
472
- const regexGroupOld = /^(\d{1,15})-(\d+)@g\.us$/
473
- const messages = normalizeMessageContent(message)
474
- const buttonType = getButtonType(messages)
475
- const pollMessage =
476
- messages.pollCreationMessage || messages.pollCreationMessageV2 || messages.pollCreationMessageV3
477
- await authState.keys.transaction(async () => {
478
- const mediaType = getMediaType(message)
479
- if (mediaType) extraAttrs.mediatype = mediaType
480
- if (isNewsletter) {
481
- const patched = patchMessageBeforeSending? await patchMessageBeforeSending(message, []) : message
482
- const bytes = encodeNewsletterMessage(patched)
483
- binaryNodeContent.push({ tag: 'plaintext', attrs: {}, content: bytes })
484
- const stanza = {
485
- tag: 'message',
486
- attrs: {
487
- to: jid,
488
- id: msgId,
489
- type: getMessageType(message),
490
- ...(additionalAttributes || {})
491
- },
492
- content: binaryNodeContent
493
- }
494
- logger.debug({ msgId }, `sending newsletter message to ${jid}`)
495
- await sendNode(stanza)
496
- return
497
- }
498
- if (normalizeMessageContent(message)?.pinInChatMessage || normalizeMessageContent(message)?.reactionMessage) {
499
- extraAttrs['decrypt-fail'] = 'hide'
500
- }
501
- if (isGroupOrStatus &&!isRetryResend) {
502
- const [groupData, senderKeyMap] = await Promise.all([
503
- (async () => {
504
- let groupData = useCachedGroupMetadata && cachedGroupMetadata? await cachedGroupMetadata(jid) : undefined
505
- if (groupData && Array.isArray(groupData?.participants)) {
506
- logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
507
- } else if (!isStatus) {
508
- groupData = await groupMetadata(jid)
509
- }
510
- return groupData
511
- })(),
512
- (async () => {
513
- if (!participant &&!isStatus) {
514
- const result = await authState.keys.get('sender-key-memory', [jid])
515
- return result[jid] || {}
516
- }
517
- return {}
518
- })()
519
- ])
520
- const participantsList = groupData? groupData.participants.map(p => p.id) : []
521
- if (groupData?.ephemeralDuration && groupData.ephemeralDuration > 0) {
522
- additionalAttributes = {...additionalAttributes, expiration: groupData.ephemeralDuration.toString() }
523
- }
524
- if (isStatus && statusJidList) participantsList.push(...statusJidList)
525
- const additionalDevices = await getUSyncDevices(participantsList, false, false)
526
- devices.push(...additionalDevices)
527
- if (isGroup) {
528
- additionalAttributes = {
529
- ...additionalAttributes,
530
- addressing_mode: groupData?.addressingMode || 'lid'
531
- }
532
- }
533
- if (message?.groupStatusMessageV2 &&!message?.messageContextInfo?.messageSecret) {
534
- message = {
535
- ...message,
536
- messageContextInfo: {
537
- ...(message.messageContextInfo || {}),
538
- messageSecret: randomBytes(32)
539
- },
540
- groupStatusMessageV2: {
541
- ...message.groupStatusMessageV2,
542
- message: {
543
- ...(message.groupStatusMessageV2.message || {}),
544
- messageContextInfo: {
545
- ...(message.groupStatusMessageV2.message?.messageContextInfo || {}),
546
- messageSecret: message.messageContextInfo?.messageSecret || randomBytes(32)
547
- }
548
- }
549
- }
550
- }
551
- }
552
- // list/buttons/template -> interactiveMessage
553
- if (message.listMessage) {
554
- const list = message.listMessage
555
- message = {
556
- interactiveMessage: {
557
- nativeFlowMessage: {
558
- buttons: [
559
- {
560
- name: 'single_select',
561
- buttonParamsJson: JSON.stringify({
562
- title: list.buttonText || 'Select',
563
- sections: (list.sections || []).map(section => ({
564
- title: section.title || '',
565
- highlight_label: '',
566
- rows: (section.rows || []).map(row => ({
567
- header: '',
568
- title: row.title || '',
569
- description: row.description || '',
570
- id: row.rowId || row.id || ''
571
- }))
572
- }))
573
- })
574
- }
575
- ],
576
- messageParamsJson: '',
577
- messageVersion: 1
578
- },
579
- body: { text: list.description || '' },
580
- footer: list.footerText? { text: list.footerText } : undefined,
581
- header: list.title? { title: list.title, hasMediaAttachment: false, subtitle: '' } : undefined,
582
- contextInfo: list.contextInfo
583
- }
584
- }
585
- } else if (message.buttonsMessage) {
586
- const bMsg = message.buttonsMessage
587
- const buttons = (bMsg.buttons || []).map(btn => ({
588
- name: 'quick_reply',
589
- buttonParamsJson: JSON.stringify({
590
- display_text: btn.buttonText?.displayText || btn.buttonText || '',
591
- id: btn.buttonId || btn.buttonText?.displayText || ''
592
- })
593
- }))
594
- message = {
595
- interactiveMessage: {
596
- nativeFlowMessage: { buttons, messageParamsJson: '', messageVersion: 1 },
597
- body: { text: bMsg.contentText || bMsg.text || '' },
598
- footer: bMsg.footerText? { text: bMsg.footerText } : undefined,
599
- header: bMsg.text
600
- ? { title: bMsg.text, hasMediaAttachment: false, subtitle: '' }
601
- : bMsg.imageMessage || bMsg.videoMessage || bMsg.documentMessage
602
- ? { hasMediaAttachment: true,...(bMsg.imageMessage? { imageMessage: bMsg.imageMessage } : {}),...(bMsg.videoMessage? { videoMessage: bMsg.videoMessage } : {}) }
603
- : undefined,
604
- contextInfo: bMsg.contextInfo
605
- }
606
- }
607
- } else if (message.templateMessage) {
608
- const tmpl = message.templateMessage.hydratedTemplate || message.templateMessage.fourRowTemplate
609
- if (tmpl) {
610
- const buttons = (tmpl.hydratedButtons || [])
611
- .map(hBtn => {
612
- if (hBtn.quickReplyButton) {
613
- return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: hBtn.quickReplyButton.displayText || '', id: hBtn.quickReplyButton.id || hBtn.quickReplyButton.displayText || '' }) }
614
- } else if (hBtn.urlButton) {
615
- return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: hBtn.urlButton.displayText || '', url: hBtn.urlButton.url || '', merchant_url: hBtn.urlButton.url || '' }) }
616
- } else if (hBtn.callButton) {
617
- return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: hBtn.callButton.displayText || '', phone_number: hBtn.callButton.phoneNumber || '' }) }
618
- }
619
- return null
620
- })
621
- .filter(Boolean)
622
- message = {
623
- interactiveMessage: {
624
- nativeFlowMessage: { buttons, messageParamsJson: '', messageVersion: 1 },
625
- body: { text: tmpl.hydratedContentText || tmpl.contentText || '' },
626
- footer: tmpl.hydratedFooterText? { text: tmpl.hydratedFooterText } : undefined,
627
- header: tmpl.hydratedTitleText
628
- ? { title: tmpl.hydratedTitleText, hasMediaAttachment: false, subtitle: '' }
629
- : tmpl.imageMessage || tmpl.videoMessage || tmpl.documentMessage
630
- ? { hasMediaAttachment: true,...(tmpl.imageMessage? { imageMessage: tmpl.imageMessage } : {}),...(tmpl.videoMessage? { videoMessage: tmpl.videoMessage } : {}) }
631
- : undefined,
632
- contextInfo: tmpl.contextInfo
633
- }
634
- }
635
- }
636
- }
513
+ const meId = authState.creds.me.id
514
+ const meLid = authState.creds.me?.lid
515
+ const isRetryResend = Boolean(participant?.jid)
516
+ let shouldIncludeDeviceIdentity = isRetryResend
517
+ const statusJid = 'status@broadcast'
518
+ const { user, server } = (0, WABinary_1.jidDecode)(jid)
519
+ const isGroup = server === 'g.us'
520
+ const isStatus = jid === statusJid
521
+ const isLid = server === 'lid'
522
+ const isNewsletter = server === 'newsletter'
523
+ const isInterop = (0, WABinary_1.isInteropUser)(jid)
524
+ const isGroupOrStatus = isGroup || isStatus
525
+ const finalJid = jid
526
+ const iosBros = config.browser[0] === "iOS" || config.browser[1] === "Safari";
527
+ msgId = iosBros ? Utils_1.generateIOSMessageID() : msgId ?? (0, Utils_1.generateMessageIDV2)(meId)
528
+ useUserDevicesCache = useUserDevicesCache !== false
529
+ useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
530
+ const participants = []
531
+ const destinationJid = !isStatus ? finalJid : statusJid
532
+ const binaryNodeContent = []
533
+ const devices = []
534
+ let reportingMessage
535
+ const meMsg = {
536
+ deviceSentMessage: {
537
+ destinationJid,
538
+ message
539
+ },
540
+ messageContextInfo: message.messageContextInfo
541
+ }
542
+ const extraAttrs = {}
637
543
 
638
- const patched = await patchMessageBeforeSending(message)
639
- if (Array.isArray(patched)) throw new Boom('Per-jid patching is not supported in groups')
640
- const bytes = encodeWAMessage(patched)
641
- reportingMessage = patched
642
- const groupAddressingMode = additionalAttributes?.['addressing_mode'] || groupData?.addressingMode || 'lid'
643
- const groupSenderIdentity = groupAddressingMode === 'lid' && meLid? meLid : meId
644
- const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({
645
- group: destinationJid,
646
- data: bytes,
647
- meId: groupSenderIdentity
648
- })
649
- const senderKeyRecipients = []
650
- for (const device of devices) {
651
- const deviceJid = device.jid
652
- const hasKey =!!senderKeyMap[deviceJid]
653
- if (!hasKey ||!!participant &&!isHostedLidUser(deviceJid) &&!isHostedPnUser(deviceJid) && device.device!== 99) {
654
- senderKeyRecipients.push(deviceJid)
655
- senderKeyMap[deviceJid] = true
656
- }
657
- }
658
- if (senderKeyRecipients.length) {
659
- logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending new sender key')
660
- const senderKeyMsg = {
661
- senderKeyDistributionMessage: {
662
- axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
663
- groupId: destinationJid
664
- }
665
- }
666
- await assertSessions(senderKeyRecipients)
667
- const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
668
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
669
- participants.push(...result.nodes)
670
- }
671
- binaryNodeContent.push({ tag: 'enc', attrs: { v: '2', type: 'skmsg',...extraAttrs }, content: ciphertext })
672
- await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
673
- } else {
674
- let ownId = meId
675
- if (isLid && meLid) {
676
- ownId = meLid
677
- logger.debug({ to: jid, ownId }, 'Using LID identity for @lid conversation')
678
- } else {
679
- logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation')
680
- }
681
- const { user: ownUser } = jidDecode(ownId)
682
- if (!participant) {
683
- const patchedForReporting = await patchMessageBeforeSending(message, [jid])
684
- reportingMessage = Array.isArray(patchedForReporting)
685
- ? patchedForReporting.find(item => item.recipientJid === jid) || patchedForReporting[0]
686
- : patchedForReporting
687
- }
688
- if (!isRetryResend) {
689
- const targetUserServer = isLid? 'lid' : isInterop? 'interop' : 's.whatsapp.net'
690
- devices.push({ user, device: 0, jid: jidEncode(user, targetUserServer, 0) })
691
- if (user!== ownUser &&!isInterop) {
692
- const ownUserServer = isLid? 'lid' : 's.whatsapp.net'
693
- const ownUserForAddressing = isLid && meLid? jidDecode(meLid).user : jidDecode(meId).user
694
- devices.push({ user: ownUserForAddressing, device: 0, jid: jidEncode(ownUserForAddressing, ownUserServer, 0) })
695
- }
696
- if (additionalAttributes?.['category']!== 'peer' &&!isInterop) {
697
- devices.length = 0
698
- const senderIdentity = isLid && meLid
699
- ? jidEncode(jidDecode(meLid)?.user, 'lid', undefined)
700
- : jidEncode(jidDecode(meId)?.user, 's.whatsapp.net', undefined)
701
- const sessionDevices = await getUSyncDevices([senderIdentity, jid], false, false)
702
- devices.push(...sessionDevices)
703
- logger.debug({ deviceCount: devices.length, devices: devices.map(d => `${d.user}:${d.device}@${jidDecode(d.jid)?.server}`) }, 'Device enumeration complete with unified addressing')
704
- }
705
- }
706
- const allRecipients = []
707
- const meRecipients = []
708
- const otherRecipients = []
709
- const { user: mePnUser } = jidDecode(meId)
710
- const { user: meLidUser } = meLid? jidDecode(meLid) : { user: null }
711
- for (const { user, jid } of devices) {
712
- /** noSelfSync: opsi untuk skip sync pesan ke device lain milik akun sendiri (private chat) */
713
- const isExactSenderDevice = jid === meId || (meLid && jid === meLid)
714
- if (isExactSenderDevice) {
715
- logger.debug({ jid, meId, meLid }, 'Skipping exact sender device (whatsmeow pattern)')
716
- continue
717
- }
718
- const isMe = user === mePnUser || user === meLidUser
719
- let ptcp = false
720
- if (noSelfSync) {
721
- if (!isJidGroup(jid) && !isStatus) {
722
- if (!(!isMe)) ptcp = true
723
- } else {
724
- ptcp = false
725
- }
544
+ const regexGroupOld = /^(\d{1,15})-(\d+)@g\.us$/
545
+
546
+ const messages = Utils_1.normalizeMessageContent(message)
547
+
548
+ const buttonType = getButtonType(messages)
549
+ const pollMessage = messages.pollCreationMessage || messages.pollCreationMessageV2 || messages.pollCreationMessageV3
550
+ await authState.keys.transaction(async () => {
551
+ const mediaType = getMediaType(message)
552
+ if (mediaType) {
553
+ extraAttrs['mediatype'] = mediaType
554
+ }
555
+ if (isNewsletter) {
556
+ const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
557
+ const bytes = (0, Utils_1.encodeNewsletterMessage)(patched)
558
+ binaryNodeContent.push({
559
+ tag: 'plaintext',
560
+ attrs: {},
561
+ content: bytes
562
+ })
563
+ const stanza = {
564
+ tag: 'message',
565
+ attrs: {
566
+ to: jid,
567
+ id: msgId,
568
+ type: getTypeMessage(message),
569
+ ...(additionalAttributes || {})
570
+ },
571
+ content: binaryNodeContent
572
+ }
573
+ logger.debug({ msgId }, `sending newsletter message to ${jid}`)
574
+ await sendNode(stanza)
575
+ return
576
+ }
577
+ if (
578
+ (0, Utils_1.normalizeMessageContent)(message)?.pinInChatMessage ||
579
+ (0, Utils_1.normalizeMessageContent)(message)?.reactionMessage
580
+ ) {
581
+ extraAttrs['decrypt-fail'] = 'hide' // todo: expand for reactions and other types
582
+ }
583
+ if (isGroupOrStatus && !isRetryResend) {
584
+ const [groupData, senderKeyMap] = await Promise.all([
585
+ (async () => {
586
+ let groupData = useCachedGroupMetadata && cachedGroupMetadata ? await cachedGroupMetadata(jid) : undefined
587
+ const looksStale =
588
+ groupData &&
589
+ typeof groupData.size === 'number' &&
590
+ Array.isArray(groupData.participants) &&
591
+ groupData.participants.length < groupData.size
592
+ if (groupData && Array.isArray(groupData?.participants) && !looksStale) {
593
+ logger.trace({ jid, participants: groupData.participants.length }, 'using cached group metadata')
594
+ } else if (!isStatus) {
595
+ if (looksStale) {
596
+ logger.debug(
597
+ { jid, cached: groupData.participants.length, expected: groupData.size },
598
+ 'cached group metadata looks stale, re-fetching'
599
+ )
600
+ }
601
+ groupData = await groupMetadata(jid) // TODO: start storing group participant list + addr mode in Signal & stop relying on this
602
+ }
603
+ return groupData
604
+ })(),
605
+ (async () => {
606
+ if (!participant && !isStatus) {
607
+ // what if sender memory is less accurate than the cached metadata
608
+ // on participant change in group, we should do sender memory manipulation
609
+ const result = await authState.keys.get('sender-key-memory', [jid]) // TODO: check out what if the sender key memory doesn't include the LID stuff now?
610
+ return result[jid] || {}
611
+ }
612
+ return {}
613
+ })()
614
+ ])
615
+ const participantsList = groupData ? groupData.participants.map(p => p.id) : []
616
+ if (groupData?.ephemeralDuration && groupData.ephemeralDuration > 0) {
617
+ additionalAttributes = {
618
+ ...additionalAttributes,
619
+ expiration: groupData.ephemeralDuration.toString()
620
+ }
621
+ }
622
+ if (isStatus && statusJidList) {
623
+ participantsList.push(...statusJidList)
624
+ }
625
+ const additionalDevices = await getUSyncDevices(participantsList, !!useUserDevicesCache, false)
626
+ devices.push(...additionalDevices)
627
+ if (isGroup) {
628
+ additionalAttributes = {
629
+ ...additionalAttributes,
630
+ addressing_mode: groupData?.addressingMode || 'lid'
631
+ }
632
+ }
633
+ if (message?.groupStatusMessageV2 && !message?.messageContextInfo?.messageSecret) {
634
+ const { randomBytes } = require('crypto')
635
+ message = {
636
+ ...message,
637
+ messageContextInfo: {
638
+ ...(message.messageContextInfo || {}),
639
+ messageSecret: randomBytes(32)
640
+ },
641
+ groupStatusMessageV2: {
642
+ ...message.groupStatusMessageV2,
643
+ message: {
644
+ ...(message.groupStatusMessageV2.message || {}),
645
+ messageContextInfo: {
646
+ ...(message.groupStatusMessageV2.message?.messageContextInfo || {}),
647
+ messageSecret: message.messageContextInfo?.messageSecret || randomBytes(32)
648
+ }
649
+ }
650
+ }
651
+ }
652
+ }
653
+ if (message.listMessage) {
654
+ const list = message.listMessage
655
+ const interactiveMessage = {
656
+ nativeFlowMessage: {
657
+ buttons: [
658
+ {
659
+ name: 'single_select',
660
+ buttonParamsJson: JSON.stringify({
661
+ title: list.buttonText || 'Select',
662
+ sections: (list.sections || []).map(section => ({
663
+ title: section.title || '',
664
+ highlight_label: '',
665
+ rows: (section.rows || []).map(row => ({
666
+ header: '',
667
+ title: row.title || '',
668
+ description: row.description || '',
669
+ id: row.rowId || row.id || ''
670
+ }))
671
+ }))
672
+ })
673
+ }
674
+ ],
675
+ messageParamsJson: '',
676
+ messageVersion: 1
677
+ },
678
+ body: { text: list.description || '' },
679
+ footer: list.footerText ? { text: list.footerText } : undefined,
680
+ header: list.title ? { title: list.title, hasMediaAttachment: false, subtitle: '' } : undefined,
681
+ contextInfo: list.contextInfo
682
+ }
683
+ message = { interactiveMessage }
684
+ } else if (message.buttonsMessage) {
685
+ const bMsg = message.buttonsMessage
686
+ const buttons = (bMsg.buttons || []).map(btn => ({
687
+ name: 'quick_reply',
688
+ buttonParamsJson: JSON.stringify({
689
+ display_text: btn.buttonText?.displayText || btn.buttonText || '',
690
+ id: btn.buttonId || btn.buttonText?.displayText || ''
691
+ })
692
+ }))
693
+ const interactiveMessage = {
694
+ nativeFlowMessage: {
695
+ buttons,
696
+ messageParamsJson: '',
697
+ messageVersion: 1
698
+ },
699
+ body: { text: bMsg.contentText || bMsg.text || '' },
700
+ footer: bMsg.footerText ? { text: bMsg.footerText } : undefined,
701
+ header: bMsg.text
702
+ ? { title: bMsg.text, hasMediaAttachment: false, subtitle: '' }
703
+ : bMsg.imageMessage || bMsg.videoMessage || bMsg.documentMessage
704
+ ? {
705
+ hasMediaAttachment: true,
706
+ ...(bMsg.imageMessage ? { imageMessage: bMsg.imageMessage } : {}),
707
+ ...(bMsg.videoMessage ? { videoMessage: bMsg.videoMessage } : {})
708
+ }
709
+ : undefined,
710
+ contextInfo: bMsg.contextInfo
711
+ }
712
+ message = { interactiveMessage }
713
+ } else if (message.templateMessage) {
714
+ const tmpl = message.templateMessage.hydratedTemplate || message.templateMessage.fourRowTemplate
715
+ if (tmpl) {
716
+ const hydratedButtons = tmpl.hydratedButtons || []
717
+ const buttons = hydratedButtons
718
+ .map(hBtn => {
719
+ if (hBtn.quickReplyButton) {
720
+ return {
721
+ name: 'quick_reply',
722
+ buttonParamsJson: JSON.stringify({
723
+ display_text: hBtn.quickReplyButton.displayText || '',
724
+ id: hBtn.quickReplyButton.id || hBtn.quickReplyButton.displayText || ''
725
+ })
726
+ }
727
+ } else if (hBtn.urlButton) {
728
+ return {
729
+ name: 'cta_url',
730
+ buttonParamsJson: JSON.stringify({
731
+ display_text: hBtn.urlButton.displayText || '',
732
+ url: hBtn.urlButton.url || '',
733
+ merchant_url: hBtn.urlButton.url || ''
734
+ })
735
+ }
736
+ } else if (hBtn.callButton) {
737
+ return {
738
+ name: 'cta_call',
739
+ buttonParamsJson: JSON.stringify({
740
+ display_text: hBtn.callButton.displayText || '',
741
+ phone_number: hBtn.callButton.phoneNumber || ''
742
+ })
743
+ }
744
+ }
745
+ return null
746
+ })
747
+ .filter(Boolean)
748
+ const interactiveMessage = {
749
+ nativeFlowMessage: {
750
+ buttons,
751
+ messageParamsJson: '',
752
+ messageVersion: 1
753
+ },
754
+ body: { text: tmpl.hydratedContentText || tmpl.contentText || '' },
755
+ footer: tmpl.hydratedFooterText ? { text: tmpl.hydratedFooterText } : undefined,
756
+ header: tmpl.hydratedTitleText
757
+ ? { title: tmpl.hydratedTitleText, hasMediaAttachment: false, subtitle: '' }
758
+ : tmpl.imageMessage || tmpl.videoMessage || tmpl.documentMessage
759
+ ? {
760
+ hasMediaAttachment: true,
761
+ ...(tmpl.imageMessage ? { imageMessage: tmpl.imageMessage } : {}),
762
+ ...(tmpl.videoMessage ? { videoMessage: tmpl.videoMessage } : {})
763
+ }
764
+ : undefined,
765
+ contextInfo: tmpl.contextInfo
766
+ }
767
+ message = { interactiveMessage }
768
+ }
769
+ }
770
+ const patched = await patchMessageBeforeSending(message)
771
+ if (Array.isArray(patched)) {
772
+ throw new boom_1.Boom('Per-jid patching is not supported in groups')
773
+ }
774
+ const bytes = (0, Utils_1.encodeWAMessage)(patched)
775
+ reportingMessage = patched
776
+ const groupAddressingMode = additionalAttributes?.['addressing_mode'] || groupData?.addressingMode || 'lid'
777
+ const groupSenderIdentity = groupAddressingMode === 'lid' && meLid ? meLid : meId
778
+ const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({
779
+ group: destinationJid,
780
+ data: bytes,
781
+ meId: groupSenderIdentity
782
+ })
783
+ const senderKeyRecipients = []
784
+ for (const device of devices) {
785
+ const deviceJid = device.jid
786
+ const hasKey = !!senderKeyMap[deviceJid]
787
+ if (
788
+ (!hasKey || !!participant) &&
789
+ !(0, WABinary_1.isHostedLidUser)(deviceJid) &&
790
+ !(0, WABinary_1.isHostedPnUser)(deviceJid) &&
791
+ device.device !== 99
792
+ ) {
793
+ //todo: revamp all this logic
794
+ // the goal is to follow with what I said above for each group, and instead of a true false map of ids, we can set an array full of those the app has already sent pkmsgs
795
+ senderKeyRecipients.push(deviceJid)
796
+ senderKeyMap[deviceJid] = true
797
+ }
798
+ }
799
+ if (senderKeyRecipients.length) {
800
+ logger.debug({ senderKeyJids: senderKeyRecipients }, 'sending new sender key')
801
+ const senderKeyMsg = {
802
+ senderKeyDistributionMessage: {
803
+ axolotlSenderKeyDistributionMessage: senderKeyDistributionMessage,
804
+ groupId: destinationJid
805
+ }
806
+ }
807
+ const senderKeySessionTargets = senderKeyRecipients
808
+ await assertSessions(senderKeySessionTargets)
809
+ const result = await createParticipantNodes(senderKeyRecipients, senderKeyMsg, extraAttrs)
810
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || result.shouldIncludeDeviceIdentity
811
+ participants.push(...result.nodes)
812
+ }
813
+ binaryNodeContent.push({
814
+ tag: 'enc',
815
+ attrs: { v: '2', type: 'skmsg', ...extraAttrs },
816
+ content: ciphertext
817
+ })
818
+ await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } })
819
+ } else {
820
+ // ADDRESSING CONSISTENCY: Match own identity to conversation context
821
+ // TODO: investigate if this is true
822
+ let ownId = meId
823
+ if (isLid && meLid) {
824
+ ownId = meLid
825
+ logger.debug({ to: jid, ownId }, 'Using LID identity for @lid conversation')
826
+ } else {
827
+ logger.debug({ to: jid, ownId }, 'Using PN identity for @s.whatsapp.net conversation')
828
+ }
829
+ const { user: ownUser } = (0, WABinary_1.jidDecode)(ownId)
830
+ if (!participant) {
831
+ const patchedForReporting = await patchMessageBeforeSending(message, [jid])
832
+ reportingMessage = Array.isArray(patchedForReporting)
833
+ ? patchedForReporting.find(item => item.recipientJid === jid) || patchedForReporting[0]
834
+ : patchedForReporting
835
+ }
836
+ if (!isRetryResend) {
837
+ const targetUserServer = isLid ? 'lid' : isInterop ? 'interop' : 's.whatsapp.net'
838
+ devices.push({
839
+ user,
840
+ device: 0,
841
+ jid: (0, WABinary_1.jidEncode)(user, targetUserServer, 0) // rajeh, todo: this entire logic is convoluted and weird.
842
+ })
843
+ // Interop contacts don't receive a copy to own devices — native WA sends only one flat <enc>
844
+ if (user !== ownUser && !isInterop) {
845
+ const ownUserServer = isLid ? 'lid' : 's.whatsapp.net'
846
+ const ownUserForAddressing =
847
+ isLid && meLid ? (0, WABinary_1.jidDecode)(meLid).user : (0, WABinary_1.jidDecode)(meId).user
848
+ devices.push({
849
+ user: ownUserForAddressing,
850
+ device: 0,
851
+ jid: (0, WABinary_1.jidEncode)(ownUserForAddressing, ownUserServer, 0)
852
+ })
853
+ }
854
+ if (additionalAttributes?.['category'] !== 'peer' && !isInterop) {
855
+ // Clear placeholders and enumerate actual devices
856
+ devices.length = 0
857
+ // Use conversation-appropriate sender identity
858
+ const senderIdentity =
859
+ isLid && meLid
860
+ ? (0, WABinary_1.jidEncode)((0, WABinary_1.jidDecode)(meLid)?.user, 'lid', undefined)
861
+ : (0, WABinary_1.jidEncode)((0, WABinary_1.jidDecode)(meId)?.user, 's.whatsapp.net', undefined)
862
+ // Enumerate devices for sender and target with consistent addressing
863
+ const sessionDevices = await getUSyncDevices([senderIdentity, jid], true, false)
864
+ devices.push(...sessionDevices)
865
+ logger.debug(
866
+ {
867
+ deviceCount: devices.length,
868
+ devices: devices.map(d => `${d.user}:${d.device}@${(0, WABinary_1.jidDecode)(d.jid)?.server}`)
869
+ },
870
+ 'Device enumeration complete with unified addressing'
871
+ )
872
+ }
873
+ }
874
+ const allRecipients = []
875
+ const meRecipients = []
876
+ const otherRecipients = []
877
+ const { user: mePnUser } = (0, WABinary_1.jidDecode)(meId)
878
+ const { user: meLidUser } = meLid ? (0, WABinary_1.jidDecode)(meLid) : { user: null }
879
+ for (const { user, jid } of devices) {
880
+ const isExactSenderDevice = jid === meId || (meLid && jid === meLid);
881
+ if (isExactSenderDevice) continue;
882
+ const isMe = user === mePnUser || user === meLidUser;
883
+ let ptcp = false;
884
+ if (participant) {
885
+ if (!(!isMe)) ptcp = true;
726
886
  }
727
887
  if (!ptcp) {
728
- if (isMe) {
729
- meRecipients.push(jid)
730
- } else {
731
- otherRecipients.push(jid)
732
- }
733
- allRecipients.push(jid)
888
+ if (isMe) meRecipients.push(jid);
889
+ else otherRecipients.push(jid);
890
+ allRecipients.push(jid);
734
891
  }
735
892
  }
736
- // Fix: detect silent delivery failure — if the destination is genuinely
737
- // someone else (not our own number) and device resolution produced zero
738
- // recipient devices for them, don't proceed as if the send succeeded.
739
- const isSelfChatDestination = user === mePnUser || user === meLidUser
740
- if (!isRetryResend && !isSelfChatDestination && otherRecipients.length === 0) {
741
- logger.warn({ jid, deviceCount: devices.length, noSelfSync }, 'relayMessage: no recipient devices resolved for the other party; aborting to avoid a silent no-op send')
742
- throw new Boom('No devices resolved for recipient — message was not delivered to the other party', { statusCode: 421, data: { jid } })
743
- }
744
- await assertSessions(allRecipients)
745
- const [
746
- { nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
747
- { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
748
- ] = await Promise.all([
749
- createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
750
- createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
751
- ])
752
- participants.push(...meNodes,...otherNodes)
753
- if (meRecipients.length > 0 || otherRecipients.length > 0) {
754
- extraAttrs.phash = generateParticipantHashV2([...meRecipients,...otherRecipients])
755
- }
756
- shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
757
- }
758
- if (isRetryResend) {
759
- const isParticipantLid = jidDecode(participant.jid).server === 'lid'
760
- const isMe = areJidsSameUser(participant.jid, isParticipantLid? meLid : meId)
761
- const encodedMessageToSend = isMe
762
- ? encodeWAMessage({ deviceSentMessage: { destinationJid, message } })
763
- : encodeWAMessage(message)
764
- const { type, ciphertext: encryptedContent } = await signalRepository.encryptMessage({
765
- data: encodedMessageToSend,
766
- jid: participant.jid
767
- })
768
- binaryNodeContent.push({
769
- tag: 'enc',
770
- attrs: { v: '2', type, count: (participant.count?? 0).toString() },
771
- content: encryptedContent
772
- })
773
- }
774
- if (participants.length) {
775
- if (additionalAttributes?.['category'] === 'peer') {
776
- const peerNode = participants[0]?.content?.[0]
777
- if (peerNode) binaryNodeContent.push(peerNode)
778
- } else if (isInterop) {
779
- const recipientNode = participants.find(p => isInteropUser(p?.attrs?.jid))
780
- const encNode = (recipientNode?? participants[0])?.content?.[0]
781
- if (encNode) binaryNodeContent.push(encNode)
782
- } else {
783
- binaryNodeContent.push({ tag: 'participants', attrs: {}, content: participants })
893
+ await assertSessions(allRecipients)
894
+ const [
895
+ { nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
896
+ { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
897
+ ] = await Promise.all([
898
+ // For own devices: use DSM if available (1:1 chats only)
899
+ createParticipantNodes(meRecipients, meMsg || message, extraAttrs),
900
+ createParticipantNodes(otherRecipients, message, extraAttrs, meMsg)
901
+ ])
902
+ participants.push(...meNodes)
903
+ participants.push(...otherNodes)
904
+ if (meRecipients.length > 0 || otherRecipients.length > 0) {
905
+ extraAttrs['phash'] = (0, Utils_1.generateParticipantHashV2)([...meRecipients, ...otherRecipients])
906
+ }
907
+ shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
908
+ }
909
+ if (participants.length) {
910
+ if (additionalAttributes?.['category'] === 'peer') {
911
+ const peerNode = participants[0]?.content?.[0]
912
+ if (peerNode) {
913
+ binaryNodeContent.push(peerNode) // push only enc
914
+ }
915
+ } else if (isInterop) {
916
+ // Flat <enc> directly on <message> — no <participants> wrapper (native WA behavior).
917
+ // Find the enc for the actual interop recipient, not own-device DSM copy.
918
+ const recipientNode = participants.find(p => (0, WABinary_1.isInteropUser)(p?.attrs?.jid))
919
+ const encNode = (recipientNode ?? participants[0])?.content?.[0]
920
+ if (encNode) {
921
+ binaryNodeContent.push(encNode)
922
+ }
923
+ } else {
924
+ binaryNodeContent.push({
925
+ tag: 'participants',
926
+ attrs: {},
927
+ content: participants
928
+ })
929
+ }
930
+ }
931
+ const stanza = {
932
+ tag: 'message',
933
+ attrs: {
934
+ id: msgId,
935
+ to: destinationJid,
936
+ type: getTypeMessage(message),
937
+ ...(additionalAttributes || {})
938
+ },
939
+ content: binaryNodeContent
940
+ }
941
+ if (shouldIncludeDeviceIdentity) {
942
+ stanza.content.push({
943
+ tag: 'device-identity',
944
+ attrs: {},
945
+ content: (0, Utils_1.encodeSignedDeviceIdentity)(authState.creds.account, true)
946
+ })
947
+ logger.debug({ jid }, 'adding device identity')
784
948
  }
785
- }
786
- const stanza = {
787
- tag: 'message',
788
- attrs: { id: msgId, to: destinationJid, type: getMessageType(message),...(additionalAttributes || {}) },
789
- content: binaryNodeContent
790
- }
791
- if (shouldIncludeDeviceIdentity) {
792
- stanza.content.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
793
- logger.debug({ jid }, 'adding device identity')
794
- }
795
949
 
796
- if (isGroup && regexGroupOld.test(jid) &&!message.reactionMessage) {
797
- stanza.content.push({ tag: 'multicast', attrs: {} })
798
- }
799
- if (pollMessage || messages.eventMessage) {
800
- stanza.content.push({
801
- tag: 'meta',
802
- attrs: messages.eventMessage
803
- ? { event_type: 'creation' }
804
- : isNewsletter
805
- ? { polltype: 'creation', contenttype: pollMessage?.pollContentType === 2? 'image' : 'text' }
806
- : { polltype: 'creation' }
807
- })
808
- }
809
- if (!isNewsletter &&!isRetryResend && reportingMessage?.messageContextInfo?.messageSecret && shouldIncludeReportingToken(reportingMessage)) {
810
- try {
811
- const encoded = encodeWAMessage(reportingMessage)
812
- const reportingKey = { id: msgId, fromMe: true, remoteJid: destinationJid, participant: participant?.jid }
813
- const reportingNode = await getMessageReportingToken(encoded, reportingMessage, reportingKey)
814
- if (reportingNode) {
815
- stanza.content.push(reportingNode)
816
- logger.trace({ jid }, 'added reporting token to message')
950
+ if (isGroup && regexGroupOld.test(jid) && !message.reactionMessage) {
951
+ stanza.content.push({
952
+ tag: 'multicast',
953
+ attrs: {}
954
+ })
955
+ }
956
+
957
+ if (pollMessage || messages.eventMessage) {
958
+ stanza.content.push({
959
+ tag: 'meta',
960
+ attrs: messages.eventMessage
961
+ ? {
962
+ event_type: 'creation'
963
+ }
964
+ : isNewsletter
965
+ ? {
966
+ polltype: 'creation',
967
+ contenttype: pollMessage?.pollContentType === 2 ? 'image' : 'text'
968
+ }
969
+ : {
970
+ polltype: 'creation'
971
+ }
972
+ })
973
+ }
974
+ if (
975
+ !isNewsletter &&
976
+ !isRetryResend &&
977
+ reportingMessage?.messageContextInfo?.messageSecret &&
978
+ (0, reporting_utils_1.shouldIncludeReportingToken)(reportingMessage)
979
+ ) {
980
+ try {
981
+ const encoded = (0, Utils_1.encodeWAMessage)(reportingMessage)
982
+ const reportingKey = {
983
+ id: msgId,
984
+ fromMe: true,
985
+ remoteJid: destinationJid,
986
+ participant: participant?.jid
987
+ }
988
+ const reportingNode = await (0, reporting_utils_1.getMessageReportingToken)(
989
+ encoded,
990
+ reportingMessage,
991
+ reportingKey
992
+ )
993
+ if (reportingNode) {
994
+ stanza.content.push(reportingNode)
995
+ logger.trace({ jid }, 'added reporting token to message')
996
+ }
997
+ } catch (error) {
998
+ logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
999
+ }
1000
+ }
1001
+
1002
+ let didPushAdditional = false
1003
+ if (!isNewsletter && buttonType) {
1004
+ const buttonsNode = getButtonArgs(messages)
1005
+ const filteredButtons = WABinary_1.getBinaryFilteredButtons(additionalNodes ? additionalNodes : [])
1006
+
1007
+ if (filteredButtons) {
1008
+ stanza.content.push(...additionalNodes)
1009
+ didPushAdditional = true
1010
+ } else {
1011
+ stanza.content.push(buttonsNode)
817
1012
  }
818
- } catch (error) {
819
- logger.warn({ jid, trace: error?.stack }, 'failed to attach reporting token')
820
1013
  }
821
- }
822
- let didPushAdditional = false
823
- if (!isNewsletter && buttonType) {
824
- const buttonsNode = getButtonArgs(messages)
825
- const filteredButtons = getBinaryNodeFilter(additionalNodes? additionalNodes : [])
826
- if (filteredButtons) {
1014
+ if (!config.aiLabel && (0, WABinary_1.isPnUser)(destinationJid)) {
1015
+ const alreadyHasBizBot =
1016
+ WABinary_1.getBinaryFilteredBizBot(additionalNodes || []) ||
1017
+ WABinary_1.getBinaryFilteredBizBot(stanza.content)
1018
+ if (!alreadyHasBizBot) {
1019
+ stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
1020
+ }
1021
+ } else if (config.aiLabel && !isGroup && !isStatus && !isNewsletter) {
1022
+ const existingBizBot = WABinary_1.getBinaryFilteredBizBot(additionalNodes || [])
1023
+ if (!existingBizBot) {
1024
+ stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
1025
+ }
1026
+ }
1027
+ // WA Web never attaches tctoken to peer (AppStateSync) messages — server rejects with 479
1028
+ const isPeerMessage = additionalAttributes?.['category'] === 'peer'
1029
+ const is1on1Send = !isGroup && !isRetryResend && !isStatus && !isNewsletter && !isPeerMessage
1030
+ // Resolve destination to LID for tctoken storage — matches Signal session key pattern
1031
+ const tcTokenJid = is1on1Send
1032
+ ? await (0, tc_token_utils_1.resolveTcTokenJid)(destinationJid, getLIDForPN)
1033
+ : destinationJid
1034
+ const contactTcTokenData = is1on1Send ? await authState.keys.get('tctoken', [tcTokenJid]) : {}
1035
+ const existingTokenEntry = contactTcTokenData[tcTokenJid]
1036
+ let tcTokenBuffer = existingTokenEntry?.token
1037
+ // Treat expired tokens the same as missing — clear from cache
1038
+ if (tcTokenBuffer?.length && (0, tc_token_utils_1.isTcTokenExpired)(existingTokenEntry?.timestamp)) {
1039
+ logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing')
1040
+ tcTokenBuffer = undefined
1041
+ const cleared =
1042
+ existingTokenEntry?.senderTimestamp !== undefined
1043
+ ? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp }
1044
+ : null
1045
+ try {
1046
+ await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } })
1047
+ } catch (err) {
1048
+ logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup')
1049
+ }
1050
+ }
1051
+ if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
1052
+ stanza.content.push({
1053
+ tag: 'tctoken',
1054
+ attrs: {},
1055
+ content: tcTokenBuffer
1056
+ })
1057
+ }
1058
+ if (additionalNodes && additionalNodes.length > 0 && !didPushAdditional) {
827
1059
  stanza.content.push(...additionalNodes)
828
- didPushAdditional = true
829
- } else {
830
- stanza.content.push(buttonsNode)
831
1060
  }
832
- }
833
- if (!aiLabel && isPnUser(destinationJid)) {
834
- const alreadyHasBizBot = getBinaryFilteredBizBot(additionalNodes || []) || getBinaryFilteredBizBot(stanza.content)
835
- if (!alreadyHasBizBot) stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
836
- } else if (aiLabel &&!isGroup &&!isStatus &&!isNewsletter) {
837
- const existingBizBot = getBinaryFilteredBizBot(additionalNodes || [])
838
- if (!existingBizBot) stanza.content.push({ tag: 'bot', attrs: { biz_bot: '1' } })
839
- }
840
- const isPeerMessage = additionalAttributes?.['category'] === 'peer'
841
- const is1on1Send =!isGroup &&!isRetryResend &&!isStatus &&!isNewsletter &&!isPeerMessage
842
- const tcTokenJid = is1on1Send? await resolveTcTokenJid(destinationJid, getLIDForPN) : destinationJid
843
- const contactTcTokenData = is1on1Send? await authState.keys.get('tctoken', [tcTokenJid]) : {}
844
- const existingTokenEntry = contactTcTokenData[tcTokenJid]
845
- let tcTokenBuffer = existingTokenEntry?.token
846
- if (tcTokenBuffer?.length && isTcTokenExpired(existingTokenEntry?.timestamp)) {
847
- logger.debug({ jid: destinationJid, timestamp: existingTokenEntry?.timestamp }, 'tctoken expired, clearing')
848
- tcTokenBuffer = undefined
849
- const cleared = existingTokenEntry?.senderTimestamp!== undefined? { token: Buffer.alloc(0), senderTimestamp: existingTokenEntry.senderTimestamp } : null
850
- try {
851
- await authState.keys.set({ tctoken: { [tcTokenJid]: cleared } })
852
- } catch (err) {
853
- logger.debug({ jid: destinationJid, err: err?.message }, 'failed to persist tctoken expiry cleanup')
1061
+ logger.debug({ msgId }, `sending message to ${participants.length} devices`)
1062
+ await sendNode(stanza)
1063
+ // Register messageSecret immediately at send time so msmsg replies can be
1064
+ // decrypted even if they arrive before our own message echo comes back.
1065
+ if (message.messageContextInfo?.messageSecret) {
1066
+ setBotMessageSecret(msgId, message.messageContextInfo.messageSecret, destinationJid)
854
1067
  }
855
- }
856
- if (tcTokenBuffer?.length && sock.serverProps.privacyTokenOn1to1) {
857
- stanza.content.push({ tag: 'tctoken', attrs: {}, content: tcTokenBuffer })
858
- }
859
- if (additionalNodes && additionalNodes.length > 0 &&!didPushAdditional) {
860
- stanza.content.push(...additionalNodes)
861
- }
862
- logger.debug({ msgId }, `sending message to ${participants.length} devices`)
863
- await sendNode(stanza)
864
- if (message.messageContextInfo?.messageSecret) {
865
- setBotMessageSecret(msgId, message.messageContextInfo.messageSecret, destinationJid)
866
- }
867
- const isProtocolMsg =!!normalizeMessageContent(message)?.protocolMessage
868
- const isBotOrPSA = destinationJid === PSA_WID || isJidBot(destinationJid) || isJidMetaAI(destinationJid)
869
- if (is1on1Send &&!isProtocolMsg &&!isBotOrPSA && shouldSendNewTcToken(existingTokenEntry?.senderTimestamp) &&!inFlightTcTokenIssuance.has(tcTokenJid)) {
870
- inFlightTcTokenIssuance.add(tcTokenJid)
871
- const issueTimestamp = unixTimestampSeconds()
872
- const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
873
- resolveIssuanceJid(destinationJid, sock.serverProps.lidTrustedTokenIssueToLid, getLIDForPN, getPNForLID)
874
- .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
875
- .then(async result => {
876
- await storeTcTokensFromIqResult({ result, fallbackJid: tcTokenJid, keys: authState.keys, getLIDForPN })
877
- const currentData = await authState.keys.get('tctoken', [tcTokenJid])
878
- const currentEntry = currentData[tcTokenJid]
879
- const indexWrite = await buildMergedTcTokenIndexWrite(authState.keys, [tcTokenJid])
880
- await authState.keys.set({
881
- tctoken: {
882
- [tcTokenJid]: { token: Buffer.alloc(0),...currentEntry, senderTimestamp: issueTimestamp },
883
- ...indexWrite
884
- }
1068
+ // Fire-and-forget: issue our token to the contact AFTER message send.
1069
+ const isProtocolMsg = !!(0, Utils_1.normalizeMessageContent)(message)?.protocolMessage
1070
+ const isBotOrPSA =
1071
+ destinationJid === WABinary_1.PSA_WID ||
1072
+ (0, WABinary_1.isJidBot)(destinationJid) ||
1073
+ (0, WABinary_1.isJidMetaAI)(destinationJid)
1074
+ if (
1075
+ is1on1Send &&
1076
+ !isProtocolMsg &&
1077
+ !isBotOrPSA &&
1078
+ (0, tc_token_utils_1.shouldSendNewTcToken)(existingTokenEntry?.senderTimestamp) &&
1079
+ !inFlightTcTokenIssuance.has(tcTokenJid)
1080
+ ) {
1081
+ inFlightTcTokenIssuance.add(tcTokenJid)
1082
+ const issueTimestamp = (0, Utils_1.unixTimestampSeconds)()
1083
+ const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
1084
+ ;(0, tc_token_utils_1.resolveIssuanceJid)(
1085
+ destinationJid,
1086
+ sock.serverProps.lidTrustedTokenIssueToLid,
1087
+ getLIDForPN,
1088
+ getPNForLID
1089
+ )
1090
+ .then(issueJid => issuePrivacyTokens([issueJid], issueTimestamp))
1091
+ .then(async result => {
1092
+ await (0, tc_token_utils_1.storeTcTokensFromIqResult)({
1093
+ result,
1094
+ fallbackJid: tcTokenJid,
1095
+ keys: authState.keys,
1096
+ getLIDForPN
1097
+ })
1098
+ const currentData = await authState.keys.get('tctoken', [tcTokenJid])
1099
+ const currentEntry = currentData[tcTokenJid]
1100
+ const indexWrite = await (0, tc_token_utils_1.buildMergedTcTokenIndexWrite)(authState.keys, [tcTokenJid])
1101
+ await authState.keys.set({
1102
+ tctoken: {
1103
+ [tcTokenJid]: {
1104
+ token: Buffer.alloc(0),
1105
+ ...currentEntry,
1106
+ senderTimestamp: issueTimestamp
1107
+ },
1108
+ ...indexWrite
1109
+ }
1110
+ })
1111
+ })
1112
+ .catch(err => {
1113
+ logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed')
885
1114
  })
1115
+ .finally(() => {
1116
+ inFlightTcTokenIssuance.delete(tcTokenJid)
1117
+ })
1118
+ }
1119
+ // Add message to retry cache if enabled
1120
+ if (messageRetryManager && !participant) {
1121
+ messageRetryManager.addRecentMessage(destinationJid, msgId, message)
1122
+ }
1123
+ if (isInterop && !isRetryResend) {
1124
+ await trustInteropContact(destinationJid).catch(err => {
1125
+ logger.debug({ err, jid: destinationJid }, 'failed to trust interop contact')
886
1126
  })
887
- .catch(err => logger.debug({ jid: destinationJid, err: err?.message }, 'fire-and-forget tctoken issuance failed'))
888
- .finally(() => inFlightTcTokenIssuance.delete(tcTokenJid))
889
- }
890
- if (messageRetryManager &&!participant) {
891
- messageRetryManager.addRecentMessage(destinationJid, msgId, message)
892
- }
893
- if (isInterop &&!isRetryResend) {
894
- await trustInteropContact(destinationJid).catch(err => logger.debug({ err, jid: destinationJid }, 'failed to trust interop contact'))
895
- }
896
- }, meId)
897
- return msgId
898
- }
899
- const getMessageType = (message) => {
900
- const normalizedMessage = normalizeMessageContent(message);
901
- if (!normalizedMessage)
902
- return 'text';
903
- if (normalizedMessage.reactionMessage || normalizedMessage.encReactionMessage) {
904
- return 'reaction';
905
- }
906
- if (normalizedMessage.pollCreationMessage ||
907
- normalizedMessage.pollCreationMessageV2 ||
908
- normalizedMessage.pollCreationMessageV3 ||
909
- normalizedMessage.pollCreationMessageV4 ||
910
- normalizedMessage.pollCreationMessageV5 ||
911
- normalizedMessage.pollUpdateMessage) {
912
- return 'poll';
913
- }
914
- if (normalizedMessage.eventMessage) {
915
- return 'event';
916
- }
917
- if (getMediaType(normalizedMessage) !== '') {
918
- return 'media';
919
- }
920
- return 'text';
921
- };
922
- const getMediaType = (message) => {
923
- if (message.imageMessage) {
924
- return 'image';
925
- }
926
- else if (message.videoMessage) {
927
- return message.videoMessage.gifPlayback ? 'gif' : 'video';
928
- }
929
- else if (message.audioMessage) {
930
- return message.audioMessage.ptt ? 'ptt' : 'audio';
931
- }
932
- else if (message.contactMessage) {
933
- return 'vcard';
934
- }
935
- else if (message.documentMessage) {
936
- return 'document';
937
- }
938
- else if (message.contactsArrayMessage) {
939
- return 'contact_array';
940
- }
941
- else if (message.liveLocationMessage) {
942
- return 'livelocation';
943
- }
944
- else if (message.stickerMessage) {
945
- return 'sticker';
946
- }
947
- else if (message.listMessage) {
948
- return 'list';
949
- }
950
- else if (message.listResponseMessage) {
951
- return 'list_response';
952
- }
953
- else if (message.buttonsResponseMessage) {
954
- return 'buttons_response';
955
- }
956
- else if (message.orderMessage) {
957
- return 'order';
958
- }
959
- else if (message.productMessage) {
960
- return 'product';
961
- }
962
- else if (message.interactiveResponseMessage) {
963
- return 'native_flow_response';
964
- }
965
- else if (message.groupInviteMessage) {
966
- return 'url';
967
- }
968
- return '';
969
- };
970
- const getButtonType = (message) => {
971
- if (message.listMessage) {
972
- return 'list'
973
- }
974
- else if (message.buttonsMessage) {
975
- return 'buttons'
976
- }
977
- else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'review_and_pay') {
978
- return 'review_and_pay'
979
- }
980
- else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'review_order') {
981
- return 'review_order'
982
- }
983
- else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'payment_info') {
984
- return 'payment_info'
985
- } else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'payment_key_info') {
986
- return 'payment_key_info'
987
- } else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'payment_status') {
988
- return 'payment_status'
989
- }
990
- else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'payment_method') {
991
- return 'payment_method'
992
- }
993
- else if (message.interactiveMessage?.nativeFlowMessage?.buttons?.[0]?.name === 'catalog_message') {
994
- return 'catalog_message'
995
- }
996
- else if (message.interactiveMessage && message.interactiveMessage?.nativeFlowMessage) {
997
- return 'interactive'
998
- }
999
- else if (message.interactiveMessage?.nativeFlowMessage) {
1000
- return 'native_flow'
1001
- }
1002
- };
1003
- const getButtonArgs = (message) => {
1127
+ }
1128
+ }, meId)
1129
+ return msgId
1130
+ }
1131
+ const getTypeMessage = msg => {
1132
+ const message = Utils_1.normalizeMessageContent(msg)
1133
+ if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) {
1134
+ return 'poll'
1135
+ } else if (message.reactionMessage) {
1136
+ return 'reaction'
1137
+ } else if (message.eventMessage) {
1138
+ return 'event'
1139
+ } else if (getMediaType(message)) {
1140
+ return 'media'
1141
+ } else {
1142
+ return 'text'
1143
+ }
1144
+ }
1145
+
1146
+ const getMediaType = message => {
1147
+ if (message.imageMessage) {
1148
+ return 'image'
1149
+ } else if (message.stickerMessage) {
1150
+ return message.stickerMessage.isLottie
1151
+ ? '1p_sticker'
1152
+ : message.stickerMessage.isAvatar
1153
+ ? 'avatar_sticker'
1154
+ : 'sticker'
1155
+ } else if (message.videoMessage) {
1156
+ return message.videoMessage.gifPlayback ? 'gif' : 'video'
1157
+ } else if (message.audioMessage) {
1158
+ return message.audioMessage.ptt ? 'ptt' : 'audio'
1159
+ } else if (message.ptvMessage) {
1160
+ return 'ptv'
1161
+ } else if (message.albumMessage) {
1162
+ return 'collection'
1163
+ } else if (message.contactMessage) {
1164
+ return 'vcard'
1165
+ } else if (message.documentMessage) {
1166
+ return 'document'
1167
+ } else if (message.stickerPackMessage) {
1168
+ return 'sticker_pack'
1169
+ } else if (message.contactsArrayMessage) {
1170
+ return 'contact_array'
1171
+ } else if (message.locationMessage) {
1172
+ return 'location'
1173
+ } else if (message.liveLocationMessage) {
1174
+ return 'livelocation'
1175
+ } else if (message.listMessage) {
1176
+ return 'list'
1177
+ } else if (message.listResponseMessage) {
1178
+ return 'list_response'
1179
+ } else if (message.buttonsResponseMessage) {
1180
+ return 'buttons_response'
1181
+ } else if (message.orderMessage) {
1182
+ return 'order'
1183
+ } else if (message.productMessage) {
1184
+ return 'product'
1185
+ } else if (message.interactiveResponseMessage) {
1186
+ return 'native_flow_response'
1187
+ } else if (/https:\/\/wa\.me\/c\/\d+/.test(message.extendedTextMessage?.text)) {
1188
+ return 'cataloglink'
1189
+ } else if (/https:\/\/wa\.me\/p\/\d+\/\d+/.test(message.extendedTextMessage?.text)) {
1190
+ return 'productlink'
1191
+ } else if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) {
1192
+ return 'url'
1193
+ }
1194
+ }
1195
+ const getButtonType = message => {
1196
+ if (message.listMessage) {
1197
+ return 'list'
1198
+ } else if (message.buttonsMessage) {
1199
+ return 'buttons'
1200
+ } else if (message.interactiveMessage?.nativeFlowMessage) {
1201
+ const firstButtonName = message.interactiveMessage.nativeFlowMessage.buttons?.[0]?.name
1202
+ if (firstButtonName === 'review_and_pay') {
1203
+ return 'review_and_pay'
1204
+ } else if (firstButtonName === 'review_order') {
1205
+ return 'review_order'
1206
+ } else if (firstButtonName === 'payment_info') {
1207
+ return 'payment_info'
1208
+ } else if (firstButtonName === 'payment_status') {
1209
+ return 'payment_status'
1210
+ } else if (firstButtonName === 'payment_method') {
1211
+ return 'payment_method'
1212
+ } else if (firstButtonName === 'pix') {
1213
+ return 'pix'
1214
+ } else if (firstButtonName === 'pay') {
1215
+ return 'pay'
1216
+ }
1217
+ return 'native_flow'
1218
+ }
1219
+ }
1220
+
1221
+ const getButtonArgs = message => {
1004
1222
  const nativeFlow = message.interactiveMessage?.nativeFlowMessage
1005
1223
  const firstButtonName = nativeFlow?.buttons?.[0]?.name
1006
1224
  const nativeFlowSpecials = [
@@ -1122,282 +1340,469 @@ export const makeMessagesSocket = (config) => {
1122
1340
  }
1123
1341
  }
1124
1342
  }
1125
- const issuePrivacyTokens = async (jids, timestamp) => {
1126
- const t = (timestamp ?? unixTimestampSeconds()).toString();
1127
- const result = await query({
1128
- tag: 'iq',
1129
- attrs: {
1130
- to: S_WHATSAPP_NET,
1131
- type: 'set',
1132
- xmlns: 'privacy'
1133
- },
1134
- content: [
1135
- {
1136
- tag: 'tokens',
1137
- attrs: {},
1138
- content: jids.map(jid => ({
1139
- tag: 'token',
1140
- attrs: {
1141
- jid: jidNormalizedUser(jid),
1142
- t,
1143
- type: 'trusted_contact'
1144
- }
1145
- }))
1146
- }
1147
- ]
1148
- });
1149
- return result;
1150
- };
1151
- const waUploadToServer = getWAUploadToServer(config, refreshMediaConn);
1152
- const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update');
1153
- registerSocketEndHandler(() => {
1154
- if (!config.userDevicesCache && userDevicesCache.close) {
1155
- userDevicesCache.close();
1156
- }
1157
- mediaConn = undefined;
1158
- if (messageRetryManager) {
1159
- messageRetryManager.clear();
1160
- }
1161
- });
1162
- return {
1163
- ...sock,
1164
- userDevicesCache,
1165
- devicesMutex,
1166
- issuePrivacyTokens,
1167
- assertSessions,
1168
- relayMessage,
1169
- sendReceipt,
1170
- sendReceipts,
1171
- readMessages,
1172
- refreshMediaConn,
1173
- // Function (not getter) so the spread in chats.ts preserves the live closure binding.
1174
- getMediaHost: () => mediaHost,
1175
- waUploadToServer,
1176
- fetchPrivacySettings,
1177
- sendPeerDataOperationMessage,
1178
- createParticipantNodes,
1179
- getUSyncDevices,
1180
- messageRetryManager,
1181
- updateMemberLabel,
1182
- updateMediaMessage: async (message) => {
1183
- const content = assertMediaContent(message.message);
1184
- const mediaKey = content.mediaKey;
1185
- const meId = authState.creds.me.id;
1186
- const node = encryptMediaRetryRequest(message.key, mediaKey, meId);
1187
- let error = undefined;
1188
- await Promise.all([
1189
- sendNode(node),
1190
- waitForMsgMediaUpdate(async (update) => {
1191
- const result = update.find(c => c.key.id === message.key.id);
1192
- if (result) {
1193
- if (result.error) {
1194
- error = result.error;
1195
- }
1196
- else {
1197
- try {
1198
- const media = decryptMediaRetryData(result.media, mediaKey, result.key.id);
1199
- if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
1200
- const resultStr = proto.MediaRetryNotification.ResultType[media.result];
1201
- throw new Boom(`Media re-upload failed by device (${resultStr})`, {
1202
- data: media,
1203
- statusCode: getStatusCodeForMediaRetry(media.result) || 404
1204
- });
1205
- }
1206
- content.directPath = media.directPath;
1207
- content.url = getUrlFromDirectPath(content.directPath, mediaHost);
1208
- logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful');
1209
- }
1210
- catch (err) {
1211
- error = err;
1212
- }
1213
- }
1214
- return true;
1215
- }
1216
- })
1217
- ]);
1218
- if (error) {
1219
- throw error;
1220
- }
1221
- ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }]);
1222
- return message;
1223
- },
1224
- sendTable: async (jid, title, headers, rows, quoted, options = {}) => {
1225
- const { message, messageId } = Utils_1.generateTableContent(title, headers, rows, quoted, options)
1226
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1343
+ const issuePrivacyTokens = async (jids, timestamp) => {
1344
+ const t = (timestamp ?? (0, Utils_1.unixTimestampSeconds)()).toString()
1345
+ const result = await query({
1346
+ tag: 'iq',
1347
+ attrs: {
1348
+ to: WABinary_1.S_WHATSAPP_NET,
1349
+ type: 'set',
1350
+ xmlns: 'privacy'
1351
+ },
1352
+ content: [
1353
+ {
1354
+ tag: 'tokens',
1355
+ attrs: {},
1356
+ content: jids.map(jid => ({
1357
+ tag: 'token',
1358
+ attrs: {
1359
+ jid: (0, WABinary_1.jidNormalizedUser)(jid),
1360
+ t,
1361
+ type: 'trusted_contact'
1362
+ }
1363
+ }))
1364
+ }
1365
+ ]
1366
+ })
1367
+ return result
1368
+ }
1369
+ const getPrivacyTokens = issuePrivacyTokens
1370
+ const waUploadToServer = (0, Utils_1.getWAUploadToServer)(config, refreshMediaConn)
1371
+ const interactiveHandler = new interactive_handler_1.InteractiveMessageHandler(waUploadToServer, relayMessage, config, sock)
1372
+ const waitForMsgMediaUpdate = (0, Utils_1.bindWaitForEvent)(ev, 'messages.media-update')
1373
+ return {
1374
+ ...sock,
1375
+ issuePrivacyTokens,
1376
+ getPrivacyTokens,
1377
+ assertSessions,
1378
+ relayMessage,
1379
+ sendReceipt,
1380
+ sendReceipts,
1381
+ readMessages,
1382
+ refreshMediaConn,
1383
+ waUploadToServer,
1384
+ fetchPrivacySettings,
1385
+ sendPeerDataOperationMessage,
1386
+ createParticipantNodes,
1387
+ getUSyncDevices,
1388
+ messageRetryManager,
1389
+ updateMemberLabel,
1390
+ updateMediaMessage: async message => {
1391
+ const content = (0, Utils_1.assertMediaContent)(message.message)
1392
+ const mediaKey = content.mediaKey
1393
+ const meId = authState.creds.me.id
1394
+ const node = (0, Utils_1.encryptMediaRetryRequest)(message.key, mediaKey, meId)
1395
+ let error = undefined
1396
+ await Promise.all([
1397
+ sendNode(node),
1398
+ waitForMsgMediaUpdate(async update => {
1399
+ const result = update.find(c => c.key.id === message.key.id)
1400
+ if (result) {
1401
+ if (result.error) {
1402
+ error = result.error
1403
+ } else {
1404
+ try {
1405
+ const media = (0, Utils_1.decryptMediaRetryData)(result.media, mediaKey, result.key.id)
1406
+ if (media.result !== index_js_1.proto.MediaRetryNotification.ResultType.SUCCESS) {
1407
+ const resultStr = index_js_1.proto.MediaRetryNotification.ResultType[media.result]
1408
+ throw new boom_1.Boom(`Media re-upload failed by device (${resultStr})`, {
1409
+ data: media,
1410
+ statusCode: (0, Utils_1.getStatusCodeForMediaRetry)(media.result) || 404
1411
+ })
1412
+ }
1413
+ content.directPath = media.directPath
1414
+ content.url = (0, Utils_1.getUrlFromDirectPath)(content.directPath)
1415
+ logger.debug({ directPath: media.directPath, key: result.key }, 'media update successful')
1416
+ } catch (err) {
1417
+ error = err
1418
+ }
1419
+ }
1420
+ return true
1421
+ }
1422
+ })
1423
+ ])
1424
+ if (error) {
1425
+ throw error
1426
+ }
1427
+ ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }])
1428
+ return message
1429
+ },
1430
+
1431
+ sendGroupStatus: async (groupIdsOrContent = [], contentOrGroups = {}, opts = {}) => {
1432
+ const toGroupIdArray = input => {
1433
+ if (Array.isArray(input)) {
1434
+ return input
1435
+ }
1436
+ if (typeof input === 'string') {
1437
+ return [input]
1438
+ }
1439
+ return []
1440
+ }
1441
+ const normalizeGroupJid = value => {
1442
+ if (typeof value !== 'string') {
1443
+ return ''
1444
+ }
1445
+ const trimmed = value.trim()
1446
+ if (!trimmed) {
1447
+ return ''
1448
+ }
1449
+ if (/^\d{8,}$/.test(trimmed)) {
1450
+ return `${trimmed}@g.us`
1451
+ }
1452
+ return trimmed
1453
+ }
1454
+ const useNewSignature = Array.isArray(groupIdsOrContent) || typeof groupIdsOrContent === 'string'
1455
+ const groupIds = useNewSignature ? toGroupIdArray(groupIdsOrContent) : toGroupIdArray(contentOrGroups)
1456
+ const content = useNewSignature ? contentOrGroups || {} : groupIdsOrContent || {}
1457
+ const sendOpts = opts || {}
1458
+ const groupJids = groupIds.map(normalizeGroupJid).filter(jid => (0, WABinary_1.isJidGroup)(jid))
1459
+ const nonGroupJids = groupIds.filter(jid => !(0, WABinary_1.isJidGroup)(normalizeGroupJid(jid)))
1460
+ if (nonGroupJids.length) {
1461
+ logger.warn({ nonGroupJids }, 'ignoring non-group JIDs in sendGroupStatus')
1462
+ }
1463
+ if (!groupJids.length) {
1464
+ return []
1465
+ }
1466
+ const { allUsers } = await resolveStatusAudience(groupJids, false)
1467
+ if (!allUsers.length) {
1468
+ return []
1469
+ }
1470
+ const msg = await generateStatusMessage({
1471
+ ...content,
1472
+ backgroundColor: content.backgroundColor || getRandomHexColor(),
1473
+ font: normalizeStatusFont(content.font, logger)
1474
+ })
1475
+ await relayMessage(WABinary_1.STORIES_JID, msg.message, {
1476
+ messageId: msg.key.id,
1477
+ statusJidList: allUsers,
1478
+ ...(sendOpts.relay || {})
1479
+ })
1480
+ return groupJids
1481
+ },
1482
+ sendAlbumMessage: async (jid, medias, options = {}) => {
1483
+ const userJid = authState.creds.me.id
1484
+ for (const media of medias) {
1485
+ if (!media.image && !media.video) throw new TypeError(`medias[i] must have image or video property`)
1486
+ }
1487
+ if (medias.length < 2) throw new RangeError('Minimum 2 media')
1488
+ const time = options.delay || 500
1489
+ delete options.delay
1490
+ const album = await (0, Utils_1.generateWAMessageFromContent)(
1491
+ jid,
1492
+ {
1493
+ albumMessage: {
1494
+ expectedImageCount: medias.filter(media => media.image).length,
1495
+ expectedVideoCount: medias.filter(media => media.video).length,
1496
+ ...options
1497
+ }
1498
+ },
1499
+ { userJid, ...options }
1500
+ )
1501
+ await relayMessage(jid, album.message, { messageId: album.key.id })
1502
+ let mediaHandle
1503
+ let msg
1504
+ for (const i in medias) {
1505
+ const media = medias[i]
1506
+ if (media.image) {
1507
+ msg = await (0, Utils_1.generateWAMessage)(
1508
+ jid,
1509
+ {
1510
+ image: media.image,
1511
+ ...media,
1512
+ ...options
1513
+ },
1514
+ {
1515
+ userJid,
1516
+ upload: async (readStream, opts) => {
1517
+ const up = await waUploadToServer(readStream, {
1518
+ ...opts,
1519
+ newsletter: (0, WABinary_1.isJidNewsletter)(jid)
1520
+ })
1521
+ mediaHandle = up.handle
1522
+ return up
1523
+ },
1524
+ ...options
1525
+ }
1526
+ )
1527
+ } else if (media.video) {
1528
+ msg = await (0, Utils_1.generateWAMessage)(
1529
+ jid,
1530
+ {
1531
+ video: media.video,
1532
+ ...media,
1533
+ ...options
1534
+ },
1535
+ {
1536
+ userJid,
1537
+ upload: async (readStream, opts) => {
1538
+ const up = await waUploadToServer(readStream, {
1539
+ ...opts,
1540
+ newsletter: (0, WABinary_1.isJidNewsletter)(jid)
1541
+ })
1542
+ mediaHandle = up.handle
1543
+ return up
1544
+ },
1545
+ ...options
1546
+ }
1547
+ )
1548
+ }
1549
+ if (msg) {
1550
+ msg.message.messageContextInfo = {
1551
+ messageAssociation: {
1552
+ associationType: 1,
1553
+ parentMessageKey: album.key
1554
+ }
1555
+ }
1556
+ }
1557
+ await relayMessage(jid, msg.message, { messageId: msg.key.id })
1558
+ await (0, Utils_1.delay)(time)
1559
+ }
1560
+ return album
1561
+ },
1562
+
1563
+ sendStatusMention: async (content, jids = []) => {
1564
+ return await interactiveHandler.sendStatusWhatsApp(content, jids)
1565
+ },
1566
+ sendTable: async (jid, title, headers, rows, quoted, options = {}) => {
1567
+ const { message, messageId } = message_composer_1.generateTableContent(title, headers, rows, quoted, options)
1568
+ await relayMessage(jid, message, { messageId })
1227
1569
  return { message, messageId }
1228
1570
  },
1229
1571
  sendList: async (jid, title, items, quoted, options = {}) => {
1230
- const { message, messageId } = Utils_1.generateListContent(title, items, quoted, options)
1231
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1572
+ const { message, messageId } = message_composer_1.generateListContent(title, items, quoted, options)
1573
+ await relayMessage(jid, message, { messageId })
1232
1574
  return { message, messageId }
1233
1575
  },
1234
1576
  sendCodeBlock: async (jid, code, quoted, options = {}) => {
1235
- const { message, messageId } = Utils_1.generateCodeBlockContent(code, quoted, options)
1236
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1577
+ const { message, messageId } = message_composer_1.generateCodeBlockContent(code, quoted, options)
1578
+ await relayMessage(jid, message, { messageId })
1237
1579
  return { message, messageId }
1238
1580
  },
1239
1581
  sendLatex: async (jid, quoted, options) => {
1240
- const { message, messageId } = Utils_1.generateLatexContent(quoted, options)
1241
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1582
+ const { message, messageId } = message_composer_1.generateLatexContent(quoted, options)
1583
+ await relayMessage(jid, message, { messageId })
1242
1584
  return { message, messageId }
1243
1585
  },
1244
1586
  sendLatexImage: async (jid, quoted, options, renderLatexToPng, uploadFn) => {
1245
- const { message, messageId } = await Utils_1.generateLatexImageContent(
1587
+ const { message, messageId } = await message_composer_1.generateLatexImageContent(
1246
1588
  quoted,
1247
1589
  options,
1248
1590
  uploadFn,
1249
1591
  renderLatexToPng
1250
1592
  )
1251
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1593
+ await relayMessage(jid, message, { messageId })
1252
1594
  return { message, messageId }
1253
1595
  },
1254
1596
  sendLatexInlineImage: async (jid, quoted, options, renderLatexToPng, uploadFn) => {
1255
- const { message, messageId } = await Utils_1.generateLatexInlineImageContent(
1597
+ const { message, messageId } = await message_composer_1.generateLatexInlineImageContent(
1256
1598
  quoted,
1257
1599
  options,
1258
1600
  uploadFn,
1259
1601
  renderLatexToPng
1260
1602
  )
1261
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1603
+ await relayMessage(jid, message, { messageId })
1262
1604
  return { message, messageId }
1263
1605
  },
1264
- captureUnifiedResponse: Utils_1.captureUnifiedResponse,
1606
+ captureUnifiedResponse: message_composer_1.captureUnifiedResponse,
1265
1607
  sendUnifiedResponse: async (jid, quoted, captured) => {
1266
- const { message, messageId } = Utils_1.generateUnifiedResponseContent(quoted, captured)
1267
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1608
+ const { message, messageId } = message_composer_1.generateUnifiedResponseContent(quoted, captured)
1609
+ await relayMessage(jid, message, { messageId })
1268
1610
  return { message, messageId }
1269
1611
  },
1270
1612
  sendRichMessage: async (jid, submessages, quoted, options = {}) => {
1271
- const { message, messageId } = Utils_1.generateRichMessageContent(submessages, quoted, options)
1272
- await relayMessage(jid, message, { messageId, noSelfSync: options.noSelfSync })
1613
+ const { message, messageId } = message_composer_1.generateRichMessageContent(submessages, quoted, options)
1614
+ await relayMessage(jid, message, { messageId })
1273
1615
  return { message, messageId }
1274
1616
  },
1275
- sendMessage: async (jid, content, options = {}) => {
1276
- const userJid = authState.creds.me.id;
1277
- const luki = new imup(Utils_1, waUploadToServer, relayMessage)
1278
- const { quoted, participant = false } = options;
1279
- const messageType = luki.detectType(content);
1280
- if (typeof content === 'object' &&
1281
- 'disappearingMessagesInChat' in content &&
1282
- typeof content['disappearingMessagesInChat'] !== 'undefined' &&
1283
- isJidGroup(jid)) {
1284
- const { disappearingMessagesInChat } = content;
1285
- const value = typeof disappearingMessagesInChat === 'boolean'
1286
- ? disappearingMessagesInChat
1287
- ? WA_DEFAULT_EPHEMERAL
1288
- : 0
1289
- : disappearingMessagesInChat;
1290
- await groupToggleEphemeral(jid, value);
1291
- }
1292
- else {
1293
- if (messageType) {
1294
- switch(messageType) {
1295
- case 'PAYMENT':
1296
- const paymentContent = await luki.handlePayment(content, quoted);
1297
- return await relayMessage(jid, paymentContent, {
1298
- messageId: Utils_1.generateMessageID(),
1299
- noSelfSync: options.noSelfSync
1300
- });
1301
- case 'PRODUCT':
1302
- const productContent = await luki.handleProduct(content, jid, quoted);
1303
- const productMsg = await Utils_1.generateWAMessageFromContent(jid, productContent, { quoted });
1304
- return await relayMessage(jid, productMsg.message, {
1305
- messageId: productMsg.key.id,
1306
- noSelfSync: options.noSelfSync
1307
- });
1308
-
1309
- case 'ALBUM':
1310
- return await luki.handleAlbum(content, jid, quoted)
1311
- case 'EVENT':
1312
- return await luki.handleEvent(content, jid, quoted)
1313
- case 'POLL_RESULT':
1314
- return await luki.handlePollResult(content, jid, quoted)
1315
- case 'ORDER':
1316
- return await luki.handleOrderMessage(content, jid, quoted)
1317
- case 'GROUP_STATUS':
1318
- return await luki.handleGroupStory(content, jid, quoted)
1319
- case 'GROUP_LABEL':
1320
- return await luki.handleGbLabel(content, jid)
1321
- }
1322
- }
1323
- const fullMsg = await generateWAMessage(jid, content, {
1324
- logger,
1325
- userJid,
1326
- getUrlInfo: text => getUrlInfo(text, {
1327
- thumbnailWidth: linkPreviewImageThumbnailWidth,
1328
- fetchOpts: {
1329
- timeout: 3000,
1330
- ...(httpRequestOptions || {})
1331
- },
1332
- logger,
1333
- uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
1334
- }),
1335
- //TODO: CACHE
1336
- getProfilePicUrl: sock.profilePictureUrl,
1337
- getCallLink: sock.createCallLink,
1338
- upload: waUploadToServer,
1339
- mediaCache: config.mediaCache,
1340
- options: config.options,
1341
- messageId: generateMessageIDV2(sock.user?.id),
1342
- ...options
1343
- });
1344
- const isEventMsg = 'event' in content && !!content.event;
1345
- const isDeleteMsg = 'delete' in content && !!content.delete;
1346
- const isEditMsg = 'edit' in content && !!content.edit;
1347
- const isPinMsg = 'pin' in content && !!content.pin;
1348
- const isPollMessage = 'poll' in content && !!content.poll;
1349
- const additionalAttributes = {};
1350
- const additionalNodes = [];
1351
- // required for delete
1352
- if (isDeleteMsg) {
1353
- // if the chat is a group, and I am not the author, then delete the message as an admin
1354
- if (isJidGroup(content.delete?.remoteJid) && !content.delete?.fromMe) {
1355
- additionalAttributes.edit = '8';
1356
- }
1357
- else {
1358
- additionalAttributes.edit = '7';
1359
- }
1360
- }
1361
- else if (isEditMsg) {
1362
- additionalAttributes.edit = '1';
1363
- }
1364
- else if (isPinMsg) {
1365
- additionalAttributes.edit = '2';
1366
- }
1367
- else if (isPollMessage) {
1368
- additionalNodes.push({
1369
- tag: 'meta',
1370
- attrs: {
1371
- polltype: 'creation'
1372
- }
1373
- });
1374
- }
1375
- else if (isEventMsg) {
1376
- additionalNodes.push({
1377
- tag: 'meta',
1378
- attrs: {
1379
- event_type: 'creation'
1380
- }
1381
- });
1382
- }
1383
- await relayMessage(jid, fullMsg.message, {
1384
- messageId: fullMsg.key.id,
1385
- useCachedGroupMetadata: options.useCachedGroupMetadata,
1386
- additionalAttributes,
1387
- statusJidList: options.statusJidList,
1388
- additionalNodes: aiLabel ? additionalNodes : options.additionalNodes,
1389
- participant,
1390
- noSelfSync: options.noSelfSync
1391
- });
1392
- if (config.emitOwnEvents) {
1393
- process.nextTick(async () => {
1394
- await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1395
- });
1396
- }
1397
- return fullMsg;
1398
- }
1399
- },
1400
- sendMessageMembers: async (jid, message, options = {}) => {
1617
+ sendMessage: async (jid, content, options = {}) => {
1618
+ const userJid = authState.creds.me.id
1619
+ const iosBros = config.browser[0] === "iOS" || config.browser[1] === "Safari";
1620
+ const mId = iosBros ? Utils_1.generateIOSMessageID() : msgId ?? (0, Utils_1.generateMessageIDV2)(userJid)
1621
+ // ── Normalize: buttons[].nativeFlowInfo -> interactiveButtons ──────
1622
+ if (
1623
+ typeof content === 'object' &&
1624
+ Array.isArray(content.buttons) &&
1625
+ content.buttons.length > 0 &&
1626
+ content.buttons.some(b => b.nativeFlowInfo)
1627
+ ) {
1628
+ const interactiveButtons = content.buttons.map(b => {
1629
+ if (b.nativeFlowInfo) {
1630
+ return {
1631
+ name: b.nativeFlowInfo.name,
1632
+ buttonParamsJson: b.nativeFlowInfo.paramsJson || '{}'
1633
+ }
1634
+ }
1635
+ return {
1636
+ name: 'quick_reply',
1637
+ buttonParamsJson: JSON.stringify({
1638
+ display_text: b.buttonText?.displayText || b.buttonId || 'Button',
1639
+ id: b.buttonId || b.buttonText?.displayText || 'btn'
1640
+ })
1641
+ }
1642
+ })
1643
+ const { buttons, headerType, viewOnce, ...rest } = content
1644
+ content = { ...rest, interactiveButtons }
1645
+ }
1646
+ if (
1647
+ typeof content === 'object' &&
1648
+ 'disappearingMessagesInChat' in content &&
1649
+ typeof content['disappearingMessagesInChat'] !== 'undefined' &&
1650
+ (0, WABinary_1.isJidGroup)(jid)
1651
+ ) {
1652
+ const { disappearingMessagesInChat } = content
1653
+ const value =
1654
+ typeof disappearingMessagesInChat === 'boolean'
1655
+ ? disappearingMessagesInChat
1656
+ ? WA_DEFAULT_EPHEMERAL
1657
+ : 0
1658
+ : disappearingMessagesInChat
1659
+ await groupToggleEphemeral(jid, value)
1660
+ } else if (interactiveHandler.detectType(content)) {
1661
+ const { quoted } = options
1662
+ const messageType = interactiveHandler.detectType(content)
1663
+ switch (messageType) {
1664
+ case 'PAYMENT': {
1665
+ const paymentContent = await interactiveHandler.handlePayment(content, quoted)
1666
+ return await relayMessage(jid, paymentContent, {
1667
+ messageId: mId
1668
+ })
1669
+ }
1670
+ case 'PRODUCT': {
1671
+ const productContent = await interactiveHandler.handleProduct(content, jid, quoted)
1672
+ const productMsg = await (0, Utils_1.generateWAMessageFromContent)(jid, productContent, { quoted, userJid })
1673
+ return await relayMessage(jid, productMsg.message, {
1674
+ messageId: productMsg.key.id
1675
+ })
1676
+ }
1677
+ case 'INTERACTIVE': {
1678
+ const interactiveContent = await interactiveHandler.handleInteractive(content, jid, quoted)
1679
+ const interactiveMsg = await (0, Utils_1.generateWAMessageFromContent)(jid, interactiveContent, {
1680
+ quoted,
1681
+ userJid
1682
+ })
1683
+ return await relayMessage(jid, interactiveMsg.message, {
1684
+ messageId: interactiveMsg.key.id
1685
+ })
1686
+ }
1687
+ case 'ALBUM':
1688
+ return await interactiveHandler.handleAlbum(content, jid, quoted)
1689
+ case 'EVENT':
1690
+ return await interactiveHandler.handleEvent(content, jid, quoted)
1691
+ case 'POLL_RESULT':
1692
+ return await interactiveHandler.handlePollResult(content, jid, quoted)
1693
+ case 'GROUP_STORY':
1694
+ return await interactiveHandler.handleGroupStory(content, jid, quoted)
1695
+ case 'ORDER':
1696
+ return await interactiveHandler.handleOrderMessage(content, jid, quoted)
1697
+ case 'GROUP_LABEL':
1698
+ return await interactiveHandler.handleGbLabel(content, jid)
1699
+ }
1700
+ } else {
1701
+ let mediaHandle
1702
+ const fullMsg = await (0, Utils_1.generateWAMessage)(jid, content, {
1703
+ logger,
1704
+ userJid,
1705
+ getUrlInfo: text =>
1706
+ (0, link_preview_1.getUrlInfo)(text, {
1707
+ thumbnailWidth: linkPreviewImageThumbnailWidth,
1708
+ fetchOpts: {
1709
+ timeout: 3000,
1710
+ ...(httpRequestOptions || {})
1711
+ },
1712
+ logger,
1713
+ uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
1714
+ }),
1715
+ //TODO: CACHE
1716
+ getProfilePicUrl: sock.profilePictureUrl,
1717
+ getCallLink: sock.createCallLink,
1718
+ newsletter: (0, WABinary_1.isJidNewsletter)(jid),
1719
+ upload: async (encFilePath, opts) => {
1720
+ const up = await waUploadToServer(encFilePath, {
1721
+ ...opts,
1722
+ newsletter: (0, WABinary_1.isJidNewsletter)(jid)
1723
+ })
1724
+ mediaHandle = up.handle
1725
+ return up
1726
+ },
1727
+ mediaCache: config.mediaCache,
1728
+ options: config.options,
1729
+ messageId: mId,
1730
+ ...options
1731
+ })
1732
+ if (!mediaHandle) {
1733
+ const msgContent = fullMsg.message
1734
+ const msgTypes = ['audioMessage', 'imageMessage', 'videoMessage', 'documentMessage', 'stickerMessage']
1735
+ for (const t of msgTypes) {
1736
+ if (msgContent?.[t]?._uploadHandle) {
1737
+ mediaHandle = msgContent[t]._uploadHandle
1738
+
1739
+ delete msgContent[t]._uploadHandle
1740
+ break
1741
+ }
1742
+ }
1743
+ }
1744
+ const isEventMsg = 'event' in content && !!content.event
1745
+ const isDeleteMsg = 'delete' in content && !!content.delete
1746
+ const isEditMsg = 'edit' in content && !!content.edit
1747
+ const isPinMsg = 'pin' in content && !!content.pin
1748
+ const isPollMessage = 'poll' in content && !!content.poll
1749
+ const additionalAttributes = {}
1750
+ const additionalNodes = []
1751
+ if (isDeleteMsg) {
1752
+ if ((0, WABinary_1.isJidGroup)(content.delete?.remoteJid) && !content.delete?.fromMe) {
1753
+ additionalAttributes.edit = '8'
1754
+ } else {
1755
+ additionalAttributes.edit = '7'
1756
+ }
1757
+ } else if (isEditMsg) {
1758
+ additionalAttributes.edit = '1'
1759
+ } else if (isPinMsg) {
1760
+ additionalAttributes.edit = '2'
1761
+ } else if (isPollMessage) {
1762
+ additionalNodes.push({
1763
+ tag: 'meta',
1764
+ attrs: {
1765
+ polltype: 'creation'
1766
+ }
1767
+ })
1768
+ } else if (isEventMsg) {
1769
+ additionalNodes.push({
1770
+ tag: 'meta',
1771
+ attrs: {
1772
+ event_type: 'creation'
1773
+ }
1774
+ })
1775
+ }
1776
+ const buttonType = getButtonType(fullMsg.message)
1777
+ if (content?.audio && options?.contextInfo) {
1778
+ const msgContent = fullMsg.message
1779
+ if (msgContent?.audioMessage) {
1780
+ msgContent.audioMessage.contextInfo = options.contextInfo
1781
+ }
1782
+ }
1783
+ if (buttonType) {
1784
+ const btnNode = getButtonArgs(fullMsg.message)
1785
+ if (btnNode) additionalNodes.push(btnNode)
1786
+ }
1787
+ if (mediaHandle) {
1788
+ additionalAttributes['media_id'] = mediaHandle
1789
+ }
1790
+ await relayMessage(jid, fullMsg.message, {
1791
+ messageId: fullMsg.key.id,
1792
+ useCachedGroupMetadata: options.useCachedGroupMetadata,
1793
+ additionalAttributes,
1794
+ statusJidList: options.statusJidList,
1795
+ additionalNodes
1796
+ })
1797
+ if (config.emitOwnEvents) {
1798
+ process.nextTick(async () => {
1799
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'))
1800
+ })
1801
+ }
1802
+ return fullMsg
1803
+ }
1804
+ },
1805
+ sendMessageMembers: async (jid, message, options = {}) => {
1401
1806
  const {
1402
1807
  messageId: idm,
1403
1808
  quoted,
@@ -1412,13 +1817,10 @@ export const makeMessagesSocket = (config) => {
1412
1817
  const messages = Utils_1.normalizeMessageContent(message);
1413
1818
  const groupData = cachedGroupMetadata? await cachedGroupMetadata(jid) : await groupMetadata(jid);
1414
1819
  const isLid = groupData.addressingMode === "lid";
1415
- const adminJids = groupData.participants.filter((x) => x.admin !== null).map((y) => y.id)
1820
+ const isAdmin = groupData.participants.filter((x) => x.admin !== null).map((y) => y.id)
1416
1821
  let participantJids = groupData.participants.map(z => z.id);
1417
1822
  if (onlyMember) {
1418
- // Fix: array selalu truthy di JS meski kosong ([] ? true).
1419
- // Cek .length supaya fallback ke semua member beneran jalan
1420
- // kalau data admin kosong, bukan diam-diam kirim ke 0 orang.
1421
- participantJids = adminJids.length ? adminJids : participantJids;
1823
+ participantJids = isAdmin ? isAdmin : participantJids;
1422
1824
  }
1423
1825
  logger.info(`Sending message to ${participantJids.length} members from ${jid}`);
1424
1826
  for (let i = 0; i < participantJids.length; i++) {
@@ -1431,9 +1833,7 @@ export const makeMessagesSocket = (config) => {
1431
1833
  quoted
1432
1834
  })
1433
1835
  await relayMessage(jid, fullMsg.message, {
1434
- messageId: fullMsg.key.id,
1435
- useUserDevicesCache,
1436
- noSelfSync: options.noSelfSync
1836
+ messageId: fullMsg.key.id
1437
1837
  });
1438
1838
  logger.debug(`Message successfully sent to ${jid}`);
1439
1839
  if (delayMs && i < participantJids.length - 1) {
@@ -1448,6 +1848,6 @@ export const makeMessagesSocket = (config) => {
1448
1848
  message
1449
1849
  }, null, 4);
1450
1850
  }
1451
- };
1452
- };
1453
- //# sourceMappingURL=messages-send.js.map
1851
+ }
1852
+ }
1853
+ exports.makeMessagesSocket = makeMessagesSocket