violetics 7.0.1-alpha → 7.0.2-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +3 -2
- package/README.md +1001 -232
- package/WAProto/index.js +75379 -142631
- package/engine-requirements.js +11 -8
- package/lib/Defaults/index.js +132 -146
- package/lib/Signal/Group/ciphertext-message.js +2 -6
- package/lib/Signal/Group/group-session-builder.js +7 -42
- package/lib/Signal/Group/group_cipher.js +37 -52
- package/lib/Signal/Group/index.js +11 -57
- package/lib/Signal/Group/keyhelper.js +7 -45
- package/lib/Signal/Group/sender-chain-key.js +7 -16
- package/lib/Signal/Group/sender-key-distribution-message.js +8 -12
- package/lib/Signal/Group/sender-key-message.js +9 -13
- package/lib/Signal/Group/sender-key-name.js +2 -6
- package/lib/Signal/Group/sender-key-record.js +9 -22
- package/lib/Signal/Group/sender-key-state.js +27 -43
- package/lib/Signal/Group/sender-message-key.js +4 -8
- package/lib/Signal/libsignal.js +319 -94
- package/lib/Signal/lid-mapping.js +224 -139
- package/lib/Socket/Client/index.js +2 -19
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +53 -0
- package/lib/Socket/business.js +162 -44
- package/lib/Socket/chats.js +477 -418
- package/lib/Socket/communities.js +430 -0
- package/lib/Socket/groups.js +110 -99
- package/lib/Socket/index.js +10 -10
- package/lib/Socket/messages-recv.js +884 -561
- package/lib/Socket/messages-send.js +859 -428
- package/lib/Socket/mex.js +41 -0
- package/lib/Socket/newsletter.js +195 -390
- package/lib/Socket/socket.js +465 -315
- package/lib/Store/index.js +3 -10
- package/lib/Store/make-in-memory-store.js +73 -79
- package/lib/Store/make-ordered-dictionary.js +4 -7
- package/lib/Store/object-repository.js +2 -6
- package/lib/Types/Auth.js +1 -2
- package/lib/Types/Bussines.js +1 -0
- package/lib/Types/Call.js +1 -2
- package/lib/Types/Chat.js +7 -4
- package/lib/Types/Contact.js +1 -2
- package/lib/Types/Events.js +1 -2
- package/lib/Types/GroupMetadata.js +1 -2
- package/lib/Types/Label.js +2 -5
- package/lib/Types/LabelAssociation.js +2 -5
- package/lib/Types/Message.js +17 -9
- package/lib/Types/Newsletter.js +33 -38
- package/lib/Types/Product.js +1 -2
- package/lib/Types/Signal.js +1 -2
- package/lib/Types/Socket.js +2 -2
- package/lib/Types/State.js +12 -2
- package/lib/Types/USync.js +1 -2
- package/lib/Types/index.js +14 -31
- package/lib/Utils/auth-utils.js +228 -152
- package/lib/Utils/browser-utils.js +28 -0
- package/lib/Utils/business.js +66 -70
- package/lib/Utils/chat-utils.js +331 -249
- package/lib/Utils/crypto.js +57 -91
- package/lib/Utils/decode-wa-message.js +168 -84
- package/lib/Utils/event-buffer.js +138 -80
- package/lib/Utils/generics.js +180 -297
- package/lib/Utils/history.js +83 -49
- package/lib/Utils/identity-change-handler.js +48 -0
- package/lib/Utils/index.js +19 -33
- package/lib/Utils/link-preview.js +14 -23
- package/lib/Utils/logger.js +2 -7
- package/lib/Utils/lt-hash.js +2 -46
- package/lib/Utils/make-mutex.js +24 -47
- package/lib/Utils/message-retry-manager.js +224 -0
- package/lib/Utils/messages-media.js +501 -496
- package/lib/Utils/messages.js +1428 -362
- package/lib/Utils/noise-handler.js +145 -100
- package/lib/Utils/pre-key-manager.js +105 -0
- package/lib/Utils/process-message.js +356 -150
- package/lib/Utils/reporting-utils.js +257 -0
- package/lib/Utils/signal.js +78 -73
- package/lib/Utils/sync-action-utils.js +47 -0
- package/lib/Utils/tc-token-utils.js +17 -0
- package/lib/Utils/use-multi-file-auth-state.js +35 -45
- package/lib/Utils/validate-connection.js +91 -107
- package/lib/WABinary/constants.js +1300 -1304
- package/lib/WABinary/decode.js +26 -48
- package/lib/WABinary/encode.js +109 -155
- package/lib/WABinary/generic-utils.js +161 -149
- package/lib/WABinary/index.js +5 -21
- package/lib/WABinary/jid-utils.js +73 -40
- package/lib/WABinary/types.js +1 -2
- package/lib/WAM/BinaryInfo.js +2 -6
- package/lib/WAM/constants.js +19070 -11568
- package/lib/WAM/encode.js +17 -23
- package/lib/WAM/index.js +3 -19
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +8 -12
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -15
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -13
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -14
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -23
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -9
- package/lib/WAUSync/Protocols/index.js +4 -20
- package/lib/WAUSync/USyncQuery.js +40 -36
- package/lib/WAUSync/USyncUser.js +2 -6
- package/lib/WAUSync/index.js +3 -19
- package/lib/index.js +11 -44
- package/package.json +74 -107
- package/lib/Defaults/baileys-version.json +0 -3
- package/lib/Defaults/phonenumber-mcc.json +0 -223
- package/lib/Signal/Group/queue-job.js +0 -57
- package/lib/Socket/Client/abstract-socket-client.js +0 -13
- package/lib/Socket/Client/mobile-socket-client.js +0 -65
- package/lib/Socket/Client/web-socket-client.js +0 -118
- package/lib/Socket/groupStatus.js +0 -637
- package/lib/Socket/registration.js +0 -166
- package/lib/Socket/usync.js +0 -70
- package/lib/Store/make-cache-manager-store.js +0 -83
- package/lib/Utils/baileys-event-stream.js +0 -63
|
@@ -1,64 +1,222 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const messages_send_1 = require("./messages-send");
|
|
18
|
-
const makeMessagesRecvSocket = (config) => {
|
|
19
|
-
const {
|
|
20
|
-
logger,
|
|
21
|
-
retryRequestDelayMs,
|
|
22
|
-
maxMsgRetryCount,
|
|
23
|
-
getMessage,
|
|
24
|
-
shouldIgnoreJid
|
|
25
|
-
} = config;
|
|
26
|
-
const sock = (0, messages_send_1.makeMessagesSocket)(config);
|
|
27
|
-
const {
|
|
28
|
-
ev,
|
|
29
|
-
authState,
|
|
30
|
-
ws,
|
|
31
|
-
processingMutex,
|
|
32
|
-
signalRepository,
|
|
33
|
-
query,
|
|
34
|
-
upsertMessage,
|
|
35
|
-
resyncAppState,
|
|
36
|
-
groupMetadata,
|
|
37
|
-
onUnexpectedError,
|
|
38
|
-
assertSessions,
|
|
39
|
-
sendNode,
|
|
40
|
-
relayMessage,
|
|
41
|
-
sendReceipt,
|
|
42
|
-
uploadPreKeys,
|
|
43
|
-
createParticipantNodes,
|
|
44
|
-
getUSyncDevices,
|
|
45
|
-
sendPeerDataOperationMessage
|
|
46
|
-
} = sock;
|
|
1
|
+
import NodeCache from '@cacheable/node-cache';
|
|
2
|
+
import { Boom } from '@hapi/boom';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import Long from 'long';
|
|
5
|
+
import { proto } from '../../WAProto/index.js';
|
|
6
|
+
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js';
|
|
7
|
+
import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
|
+
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
9
|
+
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
|
+
import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
|
+
import { extractGroupMetadata } from './groups.js';
|
|
12
|
+
import { makeMessagesSocket } from './messages-send.js';
|
|
13
|
+
export const makeMessagesRecvSocket = (config) => {
|
|
14
|
+
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config;
|
|
15
|
+
const sock = makeMessagesSocket(config);
|
|
16
|
+
const { ev, authState, ws, messageMutex, notificationMutex, receiptMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, generateMessageTag, messageRetryManager } = sock;
|
|
47
17
|
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
|
|
48
|
-
const retryMutex =
|
|
49
|
-
const msgRetryCache = config.msgRetryCounterCache ||
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
18
|
+
const retryMutex = makeMutex();
|
|
19
|
+
const msgRetryCache = config.msgRetryCounterCache ||
|
|
20
|
+
new NodeCache({
|
|
21
|
+
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
22
|
+
useClones: false
|
|
23
|
+
});
|
|
24
|
+
const callOfferCache = config.callOfferCache ||
|
|
25
|
+
new NodeCache({
|
|
26
|
+
stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
|
|
27
|
+
useClones: false
|
|
28
|
+
});
|
|
29
|
+
const placeholderResendCache = config.placeholderResendCache ||
|
|
30
|
+
new NodeCache({
|
|
31
|
+
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
|
|
32
|
+
useClones: false
|
|
33
|
+
});
|
|
34
|
+
// Debounce identity-change session refreshes per JID to avoid bursts
|
|
35
|
+
const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false });
|
|
61
36
|
let sendActiveReceipts = false;
|
|
37
|
+
const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
|
|
38
|
+
if (!authState.creds.me?.id) {
|
|
39
|
+
throw new Boom('Not authenticated');
|
|
40
|
+
}
|
|
41
|
+
const pdoMessage = {
|
|
42
|
+
historySyncOnDemandRequest: {
|
|
43
|
+
chatJid: oldestMsgKey.remoteJid,
|
|
44
|
+
oldestMsgFromMe: oldestMsgKey.fromMe,
|
|
45
|
+
oldestMsgId: oldestMsgKey.id,
|
|
46
|
+
oldestMsgTimestampMs: oldestMsgTimestamp,
|
|
47
|
+
onDemandMsgCount: count
|
|
48
|
+
},
|
|
49
|
+
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
|
|
50
|
+
};
|
|
51
|
+
return sendPeerDataOperationMessage(pdoMessage);
|
|
52
|
+
};
|
|
53
|
+
const requestPlaceholderResend = async (messageKey, msgData) => {
|
|
54
|
+
if (!authState.creds.me?.id) {
|
|
55
|
+
throw new Boom('Not authenticated');
|
|
56
|
+
}
|
|
57
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
58
|
+
logger.debug({ messageKey }, 'already requested resend');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Store original message data so PDO response handler can preserve
|
|
63
|
+
// metadata (LID details, timestamps, etc.) that the phone may omit
|
|
64
|
+
await placeholderResendCache.set(messageKey?.id, msgData || true);
|
|
65
|
+
}
|
|
66
|
+
await delay(2000);
|
|
67
|
+
if (!(await placeholderResendCache.get(messageKey?.id))) {
|
|
68
|
+
logger.debug({ messageKey }, 'message received while resend requested');
|
|
69
|
+
return 'RESOLVED';
|
|
70
|
+
}
|
|
71
|
+
const pdoMessage = {
|
|
72
|
+
placeholderMessageResendRequest: [
|
|
73
|
+
{
|
|
74
|
+
messageKey
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
78
|
+
};
|
|
79
|
+
setTimeout(async () => {
|
|
80
|
+
if (await placeholderResendCache.get(messageKey?.id)) {
|
|
81
|
+
logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline');
|
|
82
|
+
await placeholderResendCache.del(messageKey?.id);
|
|
83
|
+
}
|
|
84
|
+
}, 8000);
|
|
85
|
+
return sendPeerDataOperationMessage(pdoMessage);
|
|
86
|
+
};
|
|
87
|
+
// Handles mex newsletter notifications
|
|
88
|
+
const handleMexNewsletterNotification = async (node) => {
|
|
89
|
+
const mexNode = getBinaryNodeChild(node, 'mex');
|
|
90
|
+
if (!mexNode?.content) {
|
|
91
|
+
logger.warn({ node }, 'Invalid mex newsletter notification');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let data;
|
|
95
|
+
try {
|
|
96
|
+
data = JSON.parse(mexNode.content.toString());
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
logger.error({ err: error, node }, 'Failed to parse mex newsletter notification');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const operation = data?.operation;
|
|
103
|
+
const updates = data?.updates;
|
|
104
|
+
if (!updates || !operation) {
|
|
105
|
+
logger.warn({ data }, 'Invalid mex newsletter notification content');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
logger.info({ operation, updates }, 'got mex newsletter notification');
|
|
109
|
+
switch (operation) {
|
|
110
|
+
case 'NotificationNewsletterUpdate':
|
|
111
|
+
for (const update of updates) {
|
|
112
|
+
if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
|
|
113
|
+
ev.emit('newsletter-settings.update', {
|
|
114
|
+
id: update.jid,
|
|
115
|
+
update: update.settings
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
case 'NotificationNewsletterAdminPromote':
|
|
121
|
+
for (const update of updates) {
|
|
122
|
+
if (update.jid && update.user) {
|
|
123
|
+
ev.emit('newsletter-participants.update', {
|
|
124
|
+
id: update.jid,
|
|
125
|
+
author: node.attrs.from,
|
|
126
|
+
user: update.user,
|
|
127
|
+
new_role: 'ADMIN',
|
|
128
|
+
action: 'promote'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
logger.info({ operation, data }, 'Unhandled mex newsletter notification');
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
// Handles newsletter notifications
|
|
139
|
+
const handleNewsletterNotification = async (node) => {
|
|
140
|
+
const from = node.attrs.from;
|
|
141
|
+
const child = getAllBinaryNodeChildren(node)[0];
|
|
142
|
+
const author = node.attrs.participant;
|
|
143
|
+
logger.info({ from, child }, 'got newsletter notification');
|
|
144
|
+
switch (child.tag) {
|
|
145
|
+
case 'reaction':
|
|
146
|
+
const reactionUpdate = {
|
|
147
|
+
id: from,
|
|
148
|
+
server_id: child.attrs.message_id,
|
|
149
|
+
reaction: {
|
|
150
|
+
code: getBinaryNodeChildString(child, 'reaction'),
|
|
151
|
+
count: 1
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
ev.emit('newsletter.reaction', reactionUpdate);
|
|
155
|
+
break;
|
|
156
|
+
case 'view':
|
|
157
|
+
const viewUpdate = {
|
|
158
|
+
id: from,
|
|
159
|
+
server_id: child.attrs.message_id,
|
|
160
|
+
count: parseInt(child.content?.toString() || '0', 10)
|
|
161
|
+
};
|
|
162
|
+
ev.emit('newsletter.view', viewUpdate);
|
|
163
|
+
break;
|
|
164
|
+
case 'participant':
|
|
165
|
+
const participantUpdate = {
|
|
166
|
+
id: from,
|
|
167
|
+
author,
|
|
168
|
+
user: child.attrs.jid,
|
|
169
|
+
action: child.attrs.action,
|
|
170
|
+
new_role: child.attrs.role
|
|
171
|
+
};
|
|
172
|
+
ev.emit('newsletter-participants.update', participantUpdate);
|
|
173
|
+
break;
|
|
174
|
+
case 'update':
|
|
175
|
+
const settingsNode = getBinaryNodeChild(child, 'settings');
|
|
176
|
+
if (settingsNode) {
|
|
177
|
+
const update = {};
|
|
178
|
+
const nameNode = getBinaryNodeChild(settingsNode, 'name');
|
|
179
|
+
if (nameNode?.content)
|
|
180
|
+
update.name = nameNode.content.toString();
|
|
181
|
+
const descriptionNode = getBinaryNodeChild(settingsNode, 'description');
|
|
182
|
+
if (descriptionNode?.content)
|
|
183
|
+
update.description = descriptionNode.content.toString();
|
|
184
|
+
ev.emit('newsletter-settings.update', {
|
|
185
|
+
id: from,
|
|
186
|
+
update
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case 'message':
|
|
191
|
+
const plaintextNode = getBinaryNodeChild(child, 'plaintext');
|
|
192
|
+
if (plaintextNode?.content) {
|
|
193
|
+
try {
|
|
194
|
+
const contentBuf = typeof plaintextNode.content === 'string'
|
|
195
|
+
? Buffer.from(plaintextNode.content, 'binary')
|
|
196
|
+
: Buffer.from(plaintextNode.content);
|
|
197
|
+
const messageProto = proto.Message.decode(contentBuf).toJSON();
|
|
198
|
+
const fullMessage = proto.WebMessageInfo.fromObject({
|
|
199
|
+
key: {
|
|
200
|
+
remoteJid: from,
|
|
201
|
+
id: child.attrs.message_id || child.attrs.server_id,
|
|
202
|
+
fromMe: false // TODO: is this really true though
|
|
203
|
+
},
|
|
204
|
+
message: messageProto,
|
|
205
|
+
messageTimestamp: +child.attrs.t
|
|
206
|
+
}).toJSON();
|
|
207
|
+
await upsertMessage(fullMessage, 'append');
|
|
208
|
+
logger.info('Processed plaintext newsletter message');
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logger.error({ error }, 'Failed to decode plaintext newsletter message');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
logger.warn({ node }, 'Unknown newsletter notification');
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
62
220
|
const sendMessageAck = async ({ tag, attrs, content }, errorCode) => {
|
|
63
221
|
const stanza = {
|
|
64
222
|
tag: 'ack',
|
|
@@ -67,7 +225,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
67
225
|
to: attrs.from,
|
|
68
226
|
class: tag
|
|
69
227
|
}
|
|
70
|
-
}
|
|
228
|
+
};
|
|
71
229
|
if (!!errorCode) {
|
|
72
230
|
stanza.attrs.error = errorCode.toString();
|
|
73
231
|
}
|
|
@@ -77,145 +235,204 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
77
235
|
if (!!attrs.recipient) {
|
|
78
236
|
stanza.attrs.recipient = attrs.recipient;
|
|
79
237
|
}
|
|
80
|
-
if (!!attrs.type &&
|
|
238
|
+
if (!!attrs.type &&
|
|
239
|
+
(tag !== 'message' || getBinaryNodeChild({ tag, attrs, content }, 'unavailable') || errorCode !== 0)) {
|
|
81
240
|
stanza.attrs.type = attrs.type;
|
|
82
241
|
}
|
|
83
|
-
if (tag === 'message' &&
|
|
242
|
+
if (tag === 'message' && getBinaryNodeChild({ tag, attrs, content }, 'unavailable')) {
|
|
84
243
|
stanza.attrs.from = authState.creds.me.id;
|
|
85
244
|
}
|
|
86
|
-
logger.debug({
|
|
87
|
-
recv: {
|
|
88
|
-
tag,
|
|
89
|
-
attrs
|
|
90
|
-
},
|
|
91
|
-
sent: stanza.attrs
|
|
92
|
-
}, 'sent ack');
|
|
245
|
+
logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
|
|
93
246
|
await sendNode(stanza);
|
|
94
247
|
};
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
offerContent.push({
|
|
99
|
-
tag: 'audio',
|
|
100
|
-
attrs: {
|
|
101
|
-
enc: 'opus',
|
|
102
|
-
rate: '16000'
|
|
103
|
-
}, content: undefined
|
|
104
|
-
});
|
|
105
|
-
offerContent.push({
|
|
106
|
-
tag: 'audio',
|
|
248
|
+
const rejectCall = async (callId, callFrom) => {
|
|
249
|
+
const stanza = {
|
|
250
|
+
tag: 'call',
|
|
107
251
|
attrs: {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
enc: 'vp8',
|
|
121
|
-
dec: 'vp8',
|
|
252
|
+
from: authState.creds.me.id,
|
|
253
|
+
to: callFrom
|
|
254
|
+
},
|
|
255
|
+
content: [
|
|
256
|
+
{
|
|
257
|
+
tag: 'reject',
|
|
258
|
+
attrs: {
|
|
259
|
+
'call-id': callId,
|
|
260
|
+
'call-creator': callFrom,
|
|
261
|
+
count: '0'
|
|
262
|
+
},
|
|
263
|
+
content: undefined
|
|
122
264
|
}
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
tag: 'capability',
|
|
133
|
-
attrs: {
|
|
134
|
-
ver: '1'
|
|
135
|
-
}, content: new Uint8Array([1, 4, 255, 131, 207, 4])
|
|
136
|
-
});
|
|
137
|
-
offerContent.push({
|
|
138
|
-
tag: 'encopt',
|
|
139
|
-
attrs: {
|
|
140
|
-
keygen: '2'
|
|
141
|
-
}, content: undefined
|
|
142
|
-
})
|
|
143
|
-
const encKey = (0, crypto_1.randomBytes)(32);
|
|
144
|
-
const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => (0, WABinary_1.jidEncode)(user, 's.whatsapp.net', device));
|
|
145
|
-
await assertSessions(devices, true);
|
|
146
|
-
const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
|
|
147
|
-
call: {
|
|
148
|
-
callKey: encKey
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
offerContent.push({ tag: 'destination', attrs: {}, content: destinations });
|
|
152
|
-
if (shouldIncludeDeviceIdentity) {
|
|
153
|
-
offerContent.push({
|
|
154
|
-
tag: 'device-identity',
|
|
155
|
-
attrs: {},
|
|
156
|
-
content: (0, Utils_1.encodeSignedDeviceIdentity)(authState.creds.account, true)
|
|
157
|
-
});
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
await query(stanza);
|
|
268
|
+
};
|
|
269
|
+
// Lia@Note 01-03-26 --- Source: https://github.com/koptereli/Baileys/commit/575cd41e6f01a9b3e1d7e2708c2292fa93de91f2
|
|
270
|
+
const initiateCall = async (jid, options = {}) => {
|
|
271
|
+
const meId = authState.creds.me?.id;
|
|
272
|
+
if (!meId) {
|
|
273
|
+
throw new Boom('Not authenticated');
|
|
158
274
|
}
|
|
159
|
-
const
|
|
275
|
+
const callId = randomBytes(8).toString('hex');
|
|
276
|
+
const isVideo = !!options.isVideo;
|
|
277
|
+
const isGroup = isJidGroup(jid);
|
|
278
|
+
const stanza = {
|
|
160
279
|
tag: 'call',
|
|
161
280
|
attrs: {
|
|
162
|
-
|
|
281
|
+
id: generateMessageTag(),
|
|
282
|
+
from: meId,
|
|
283
|
+
to: jid,
|
|
284
|
+
t: String(unixTimestampSeconds()),
|
|
285
|
+
...(authState.creds.me?.name ? { notify: authState.creds.me.name } : {})
|
|
163
286
|
},
|
|
164
|
-
content: [
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
287
|
+
content: [
|
|
288
|
+
{
|
|
289
|
+
tag: 'offer',
|
|
290
|
+
attrs: {
|
|
291
|
+
'call-id': callId,
|
|
292
|
+
'call-creator': meId,
|
|
293
|
+
count: '0'
|
|
294
|
+
},
|
|
295
|
+
content: [
|
|
296
|
+
{
|
|
297
|
+
tag: isVideo ? 'video' : 'audio',
|
|
298
|
+
attrs: {}
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
tag: 'net',
|
|
302
|
+
attrs: {}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
tag: 'encopt',
|
|
306
|
+
attrs: { key: randomBytes(2).toString('hex') }
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
tag: 'relaylatency',
|
|
310
|
+
attrs: {}
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
tag: 'te',
|
|
314
|
+
attrs: {}
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
};
|
|
173
320
|
await query(stanza);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
321
|
+
await callOfferCache.set(callId, {
|
|
322
|
+
chatId: jid,
|
|
323
|
+
from: meId,
|
|
324
|
+
id: callId,
|
|
325
|
+
date: new Date(),
|
|
326
|
+
offline: false,
|
|
327
|
+
status: 'offer',
|
|
177
328
|
isVideo,
|
|
178
|
-
|
|
329
|
+
isGroup,
|
|
330
|
+
groupJid: isGroup ? jid : undefined
|
|
331
|
+
});
|
|
332
|
+
// TODO: implement ICE/DTLS-SRTP call media setup once full signaling requirements are mapped.
|
|
333
|
+
return { callId, to: jid, isVideo };
|
|
179
334
|
};
|
|
180
|
-
const
|
|
181
|
-
const
|
|
335
|
+
const cancelCall = async (callId, callTo) => {
|
|
336
|
+
const meId = authState.creds.me?.id;
|
|
337
|
+
if (!meId) {
|
|
338
|
+
throw new Boom('Not authenticated');
|
|
339
|
+
}
|
|
340
|
+
const stanza = {
|
|
182
341
|
tag: 'call',
|
|
183
342
|
attrs: {
|
|
184
|
-
from:
|
|
185
|
-
to:
|
|
343
|
+
from: meId,
|
|
344
|
+
to: callTo
|
|
186
345
|
},
|
|
187
|
-
content: [
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
tag: 'terminate',
|
|
349
|
+
attrs: {
|
|
350
|
+
'call-id': callId,
|
|
351
|
+
'call-creator': meId,
|
|
352
|
+
count: '0'
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
};
|
|
197
357
|
await query(stanza);
|
|
358
|
+
await callOfferCache.del(callId);
|
|
198
359
|
};
|
|
199
360
|
const sendRetryRequest = async (node, forceIncludeKeys = false) => {
|
|
200
|
-
const { fullMessage } =
|
|
361
|
+
const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '');
|
|
201
362
|
const { key: msgKey } = fullMessage;
|
|
202
363
|
const msgId = msgKey.id;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
364
|
+
if (messageRetryManager) {
|
|
365
|
+
// Check if we've exceeded max retries using the new system
|
|
366
|
+
if (messageRetryManager.hasExceededMaxRetries(msgId)) {
|
|
367
|
+
logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing');
|
|
368
|
+
messageRetryManager.markRetryFailed(msgId);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// Increment retry count using new system
|
|
372
|
+
const retryCount = messageRetryManager.incrementRetryCount(msgId);
|
|
373
|
+
// Use the new retry count for the rest of the logic
|
|
374
|
+
const key = `${msgId}:${msgKey?.participant}`;
|
|
375
|
+
await msgRetryCache.set(key, retryCount);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// Fallback to old system
|
|
379
|
+
const key = `${msgId}:${msgKey?.participant}`;
|
|
380
|
+
let retryCount = (await msgRetryCache.get(key)) || 0;
|
|
381
|
+
if (retryCount >= maxMsgRetryCount) {
|
|
382
|
+
logger.debug({ retryCount, msgId }, 'reached retry limit, clearing');
|
|
383
|
+
await msgRetryCache.del(key);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
retryCount += 1;
|
|
387
|
+
await msgRetryCache.set(key, retryCount);
|
|
209
388
|
}
|
|
210
|
-
|
|
211
|
-
msgRetryCache.
|
|
389
|
+
const key = `${msgId}:${msgKey?.participant}`;
|
|
390
|
+
const retryCount = (await msgRetryCache.get(key)) || 1;
|
|
212
391
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
392
|
+
const fromJid = node.attrs.from;
|
|
393
|
+
// Check if we should recreate the session
|
|
394
|
+
let shouldRecreateSession = false;
|
|
395
|
+
let recreateReason = '';
|
|
396
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
397
|
+
try {
|
|
398
|
+
// Check if we have a session with this JID
|
|
399
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid);
|
|
400
|
+
const hasSession = await signalRepository.validateSession(fromJid);
|
|
401
|
+
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists);
|
|
402
|
+
shouldRecreateSession = result.recreate;
|
|
403
|
+
recreateReason = result.reason;
|
|
404
|
+
if (shouldRecreateSession) {
|
|
405
|
+
logger.debug({ fromJid, retryCount, reason: recreateReason }, 'recreating session for retry');
|
|
406
|
+
// Delete existing session to force recreation
|
|
407
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
408
|
+
forceIncludeKeys = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
logger.warn({ error, fromJid }, 'failed to check session recreation');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (retryCount <= 2) {
|
|
416
|
+
// Use new retry manager for phone requests if available
|
|
417
|
+
if (messageRetryManager) {
|
|
418
|
+
// Schedule phone request with delay (like whatsmeow)
|
|
419
|
+
messageRetryManager.schedulePhoneRequest(msgId, async () => {
|
|
420
|
+
try {
|
|
421
|
+
const requestId = await requestPlaceholderResend(msgKey);
|
|
422
|
+
logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
logger.warn({ error, msgId }, 'failed to send scheduled phone request');
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
// Fallback to immediate request
|
|
431
|
+
const msgId = await requestPlaceholderResend(msgKey);
|
|
432
|
+
logger.debug(`sendRetryRequest: requested placeholder resend for message ${msgId}`);
|
|
433
|
+
}
|
|
217
434
|
}
|
|
218
|
-
const deviceIdentity =
|
|
435
|
+
const deviceIdentity = encodeSignedDeviceIdentity(account, true);
|
|
219
436
|
await authState.keys.transaction(async () => {
|
|
220
437
|
const receipt = {
|
|
221
438
|
tag: 'receipt',
|
|
@@ -231,13 +448,15 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
231
448
|
count: retryCount.toString(),
|
|
232
449
|
id: node.attrs.id,
|
|
233
450
|
t: node.attrs.t,
|
|
234
|
-
v: '1'
|
|
451
|
+
v: '1',
|
|
452
|
+
// ADD ERROR FIELD
|
|
453
|
+
error: '0'
|
|
235
454
|
}
|
|
236
455
|
},
|
|
237
456
|
{
|
|
238
457
|
tag: 'registration',
|
|
239
458
|
attrs: {},
|
|
240
|
-
content:
|
|
459
|
+
content: encodeBigEndian(authState.creds.registrationId)
|
|
241
460
|
}
|
|
242
461
|
]
|
|
243
462
|
};
|
|
@@ -247,8 +466,8 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
247
466
|
if (node.attrs.participant) {
|
|
248
467
|
receipt.attrs.participant = node.attrs.participant;
|
|
249
468
|
}
|
|
250
|
-
if (retryCount > 1 || forceIncludeKeys) {
|
|
251
|
-
const { update, preKeys } = await
|
|
469
|
+
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
|
|
470
|
+
const { update, preKeys } = await getNextPreKeys(authState, 1);
|
|
252
471
|
const [keyId] = Object.keys(preKeys);
|
|
253
472
|
const key = preKeys[+keyId];
|
|
254
473
|
const content = receipt.content;
|
|
@@ -256,10 +475,10 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
256
475
|
tag: 'keys',
|
|
257
476
|
attrs: {},
|
|
258
477
|
content: [
|
|
259
|
-
{ tag: 'type', attrs: {}, content: Buffer.from(
|
|
478
|
+
{ tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
|
|
260
479
|
{ tag: 'identity', attrs: {}, content: identityKey.public },
|
|
261
|
-
|
|
262
|
-
|
|
480
|
+
xmppPreKey(key, +keyId),
|
|
481
|
+
xmppSignedPreKey(signedPreKey),
|
|
263
482
|
{ tag: 'device-identity', attrs: {}, content: deviceIdentity }
|
|
264
483
|
]
|
|
265
484
|
});
|
|
@@ -267,63 +486,73 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
267
486
|
}
|
|
268
487
|
await sendNode(receipt);
|
|
269
488
|
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt');
|
|
270
|
-
});
|
|
489
|
+
}, authState?.creds?.me?.id || 'sendRetryRequest');
|
|
271
490
|
};
|
|
272
491
|
const handleEncryptNotification = async (node) => {
|
|
273
492
|
const from = node.attrs.from;
|
|
274
|
-
if (from ===
|
|
275
|
-
const countChild =
|
|
493
|
+
if (from === S_WHATSAPP_NET) {
|
|
494
|
+
const countChild = getBinaryNodeChild(node, 'count');
|
|
276
495
|
const count = +countChild.attrs.value;
|
|
277
|
-
const shouldUploadMorePreKeys = count <
|
|
496
|
+
const shouldUploadMorePreKeys = count < MIN_PREKEY_COUNT;
|
|
278
497
|
logger.debug({ count, shouldUploadMorePreKeys }, 'recv pre-key count');
|
|
279
498
|
if (shouldUploadMorePreKeys) {
|
|
280
499
|
await uploadPreKeys();
|
|
281
500
|
}
|
|
282
501
|
}
|
|
283
502
|
else {
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
503
|
+
const result = await handleIdentityChange(node, {
|
|
504
|
+
meId: authState.creds.me?.id,
|
|
505
|
+
meLid: authState.creds.me?.lid,
|
|
506
|
+
validateSession: signalRepository.validateSession,
|
|
507
|
+
assertSessions,
|
|
508
|
+
debounceCache: identityAssertDebounce,
|
|
509
|
+
logger
|
|
510
|
+
});
|
|
511
|
+
if (result.action === 'no_identity_node') {
|
|
291
512
|
logger.info({ node }, 'unknown encrypt notification');
|
|
292
513
|
}
|
|
293
514
|
}
|
|
294
515
|
};
|
|
295
|
-
const handleGroupNotification = (
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
516
|
+
const handleGroupNotification = (fullNode, child, msg) => {
|
|
517
|
+
// TODO: Support PN/LID (Here is only LID now)
|
|
518
|
+
const actingParticipantLid = fullNode.attrs.participant;
|
|
519
|
+
const actingParticipantPn = fullNode.attrs.participant_pn;
|
|
520
|
+
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid;
|
|
521
|
+
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn;
|
|
522
|
+
switch (child?.tag) {
|
|
299
523
|
case 'create':
|
|
300
|
-
const metadata =
|
|
301
|
-
msg.messageStubType =
|
|
524
|
+
const metadata = extractGroupMetadata(child);
|
|
525
|
+
msg.messageStubType = WAMessageStubType.GROUP_CREATE;
|
|
302
526
|
msg.messageStubParameters = [metadata.subject];
|
|
303
|
-
msg.key = { participant: metadata.owner };
|
|
304
|
-
ev.emit('chats.upsert', [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
527
|
+
msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn };
|
|
528
|
+
ev.emit('chats.upsert', [
|
|
529
|
+
{
|
|
530
|
+
id: metadata.id,
|
|
531
|
+
name: metadata.subject,
|
|
532
|
+
conversationTimestamp: metadata.creation
|
|
533
|
+
}
|
|
534
|
+
]);
|
|
535
|
+
ev.emit('groups.upsert', [
|
|
536
|
+
{
|
|
537
|
+
...metadata,
|
|
538
|
+
author: actingParticipantLid,
|
|
539
|
+
authorPn: actingParticipantPn
|
|
540
|
+
}
|
|
541
|
+
]);
|
|
313
542
|
break;
|
|
314
543
|
case 'ephemeral':
|
|
315
544
|
case 'not_ephemeral':
|
|
316
545
|
msg.message = {
|
|
317
546
|
protocolMessage: {
|
|
318
|
-
type:
|
|
547
|
+
type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
|
|
319
548
|
ephemeralExpiration: +(child.attrs.expiration || 0)
|
|
320
549
|
}
|
|
321
550
|
};
|
|
322
551
|
break;
|
|
323
552
|
case 'modify':
|
|
324
|
-
const oldNumber =
|
|
553
|
+
const oldNumber = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid);
|
|
325
554
|
msg.messageStubParameters = oldNumber || [];
|
|
326
|
-
msg.messageStubType =
|
|
555
|
+
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER;
|
|
327
556
|
break;
|
|
328
557
|
case 'promote':
|
|
329
558
|
case 'demote':
|
|
@@ -331,181 +560,141 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
331
560
|
case 'add':
|
|
332
561
|
case 'leave':
|
|
333
562
|
const stubType = `GROUP_PARTICIPANT_${child.tag.toUpperCase()}`;
|
|
334
|
-
msg.messageStubType =
|
|
335
|
-
const participants =
|
|
563
|
+
msg.messageStubType = WAMessageStubType[stubType];
|
|
564
|
+
const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => {
|
|
565
|
+
// TODO: Store LID MAPPINGS
|
|
566
|
+
return {
|
|
567
|
+
id: attrs.jid,
|
|
568
|
+
phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined,
|
|
569
|
+
lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined,
|
|
570
|
+
admin: (attrs.type || null)
|
|
571
|
+
};
|
|
572
|
+
});
|
|
336
573
|
if (participants.length === 1 &&
|
|
337
574
|
// if recv. "remove" message and sender removed themselves
|
|
338
575
|
// mark as left
|
|
339
|
-
(
|
|
576
|
+
(areJidsSameUser(participants[0].id, actingParticipantLid) ||
|
|
577
|
+
areJidsSameUser(participants[0].id, actingParticipantPn)) &&
|
|
340
578
|
child.tag === 'remove') {
|
|
341
|
-
msg.messageStubType =
|
|
579
|
+
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE;
|
|
342
580
|
}
|
|
343
|
-
msg.messageStubParameters = participants;
|
|
581
|
+
msg.messageStubParameters = participants.map(a => JSON.stringify(a));
|
|
344
582
|
break;
|
|
345
583
|
case 'subject':
|
|
346
|
-
msg.messageStubType =
|
|
584
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT;
|
|
347
585
|
msg.messageStubParameters = [child.attrs.subject];
|
|
348
586
|
break;
|
|
349
587
|
case 'description':
|
|
350
|
-
const description =
|
|
351
|
-
msg.messageStubType =
|
|
588
|
+
const description = getBinaryNodeChild(child, 'body')?.content?.toString();
|
|
589
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION;
|
|
352
590
|
msg.messageStubParameters = description ? [description] : undefined;
|
|
353
591
|
break;
|
|
354
592
|
case 'announcement':
|
|
355
593
|
case 'not_announcement':
|
|
356
|
-
msg.messageStubType =
|
|
357
|
-
msg.messageStubParameters = [
|
|
594
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE;
|
|
595
|
+
msg.messageStubParameters = [child.tag === 'announcement' ? 'on' : 'off'];
|
|
358
596
|
break;
|
|
359
597
|
case 'locked':
|
|
360
598
|
case 'unlocked':
|
|
361
|
-
msg.messageStubType =
|
|
362
|
-
msg.messageStubParameters = [
|
|
599
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT;
|
|
600
|
+
msg.messageStubParameters = [child.tag === 'locked' ? 'on' : 'off'];
|
|
363
601
|
break;
|
|
364
602
|
case 'invite':
|
|
365
|
-
msg.messageStubType =
|
|
603
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK;
|
|
366
604
|
msg.messageStubParameters = [child.attrs.code];
|
|
367
605
|
break;
|
|
368
606
|
case 'member_add_mode':
|
|
369
607
|
const addMode = child.content;
|
|
370
608
|
if (addMode) {
|
|
371
|
-
msg.messageStubType =
|
|
609
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE;
|
|
372
610
|
msg.messageStubParameters = [addMode.toString()];
|
|
373
611
|
}
|
|
374
612
|
break;
|
|
375
613
|
case 'membership_approval_mode':
|
|
376
|
-
const approvalMode =
|
|
614
|
+
const approvalMode = getBinaryNodeChild(child, 'group_join');
|
|
377
615
|
if (approvalMode) {
|
|
378
|
-
msg.messageStubType =
|
|
616
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE;
|
|
379
617
|
msg.messageStubParameters = [approvalMode.attrs.state];
|
|
380
618
|
}
|
|
381
619
|
break;
|
|
382
620
|
case 'created_membership_requests':
|
|
383
|
-
msg.messageStubType =
|
|
384
|
-
msg.messageStubParameters = [
|
|
621
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
|
|
622
|
+
msg.messageStubParameters = [
|
|
623
|
+
JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
|
|
624
|
+
'created',
|
|
625
|
+
child.attrs.request_method
|
|
626
|
+
];
|
|
385
627
|
break;
|
|
386
628
|
case 'revoked_membership_requests':
|
|
387
|
-
const isDenied =
|
|
388
|
-
|
|
389
|
-
msg.
|
|
390
|
-
|
|
629
|
+
const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid);
|
|
630
|
+
// TODO: LIDMAPPING SUPPORT
|
|
631
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD;
|
|
632
|
+
msg.messageStubParameters = [
|
|
633
|
+
JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }),
|
|
634
|
+
isDenied ? 'revoked' : 'rejected'
|
|
635
|
+
];
|
|
391
636
|
break;
|
|
392
|
-
default:
|
|
393
|
-
// console.log("BAILEYS-DEBUG:", JSON.stringify({ ...child, content: Buffer.isBuffer(child.content) ? child.content.toString() : child.content, participant }, null, 2))
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
const handleNewsletterNotification = (id, node) => {
|
|
397
|
-
const messages = (0, WABinary_1.getBinaryNodeChild)(node, 'messages');
|
|
398
|
-
const message = (0, WABinary_1.getBinaryNodeChild)(messages, 'message');
|
|
399
|
-
const serverId = message.attrs.server_id;
|
|
400
|
-
const reactionsList = (0, WABinary_1.getBinaryNodeChild)(message, 'reactions');
|
|
401
|
-
const viewsList = (0, WABinary_1.getBinaryNodeChildren)(message, 'views_count');
|
|
402
|
-
if (reactionsList) {
|
|
403
|
-
const reactions = (0, WABinary_1.getBinaryNodeChildren)(reactionsList, 'reaction');
|
|
404
|
-
if (reactions.length === 0) {
|
|
405
|
-
ev.emit('newsletter.reaction', { id, 'server_id': serverId, reaction: { removed: true } });
|
|
406
|
-
}
|
|
407
|
-
reactions.forEach(item => {
|
|
408
|
-
var _a, _b;
|
|
409
|
-
ev.emit('newsletter.reaction', { id, 'server_id': serverId, reaction: { code: (_a = item.attrs) === null || _a === void 0 ? void 0 : _a.code, count: +((_b = item.attrs) === null || _b === void 0 ? void 0 : _b.count) } });
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
if (viewsList.length) {
|
|
413
|
-
viewsList.forEach(item => {
|
|
414
|
-
ev.emit('newsletter.view', { id, 'server_id': serverId, count: +item.attrs.count });
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
const handleMexNewsletterNotification = (id, node) => {
|
|
419
|
-
var _a;
|
|
420
|
-
const operation = node === null || node === void 0 ? void 0 : node.attrs.op_name;
|
|
421
|
-
const content = JSON.parse((_a = node === null || node === void 0 ? void 0 : node.content) === null || _a === void 0 ? void 0 : _a.toString());
|
|
422
|
-
let contentPath;
|
|
423
|
-
if (operation === Types_1.MexOperations.PROMOTE || operation === Types_1.MexOperations.DEMOTE) {
|
|
424
|
-
let action;
|
|
425
|
-
if (operation === Types_1.MexOperations.PROMOTE) {
|
|
426
|
-
action = 'promote';
|
|
427
|
-
contentPath = content.data[Types_1.XWAPaths.PROMOTE];
|
|
428
|
-
}
|
|
429
|
-
if (operation === Types_1.MexOperations.DEMOTE) {
|
|
430
|
-
action = 'demote';
|
|
431
|
-
contentPath = content.data[Types_1.XWAPaths.DEMOTE];
|
|
432
|
-
}
|
|
433
|
-
ev.emit('newsletter-participants.update', { id, author: contentPath.actor.pn, user: contentPath.user.pn, new_role: contentPath.user_new_role, action });
|
|
434
|
-
}
|
|
435
|
-
if (operation === Types_1.MexOperations.UPDATE) {
|
|
436
|
-
contentPath = content.data[Types_1.XWAPaths.METADATA_UPDATE];
|
|
437
|
-
ev.emit('newsletter-settings.update', { id, update: contentPath.thread_metadata.settings });
|
|
438
637
|
}
|
|
439
638
|
};
|
|
440
639
|
const processNotification = async (node) => {
|
|
441
|
-
var _a, _b;
|
|
442
640
|
const result = {};
|
|
443
|
-
const [child] =
|
|
641
|
+
const [child] = getAllBinaryNodeChildren(node);
|
|
444
642
|
const nodeType = node.attrs.type;
|
|
445
|
-
const from =
|
|
643
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
446
644
|
switch (nodeType) {
|
|
447
|
-
case 'privacy_token':
|
|
448
|
-
const tokenList = (0, WABinary_1.getBinaryNodeChildren)(child, 'token');
|
|
449
|
-
for (const { attrs, content } of tokenList) {
|
|
450
|
-
const jid = attrs.jid;
|
|
451
|
-
ev.emit('chats.update', [
|
|
452
|
-
{
|
|
453
|
-
id: jid,
|
|
454
|
-
tcToken: content
|
|
455
|
-
}
|
|
456
|
-
]);
|
|
457
|
-
logger.debug({ jid }, 'got privacy token update');
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
645
|
case 'newsletter':
|
|
461
|
-
handleNewsletterNotification(node
|
|
646
|
+
await handleNewsletterNotification(node);
|
|
462
647
|
break;
|
|
463
648
|
case 'mex':
|
|
464
|
-
handleMexNewsletterNotification(node
|
|
649
|
+
await handleMexNewsletterNotification(node);
|
|
465
650
|
break;
|
|
466
651
|
case 'w:gp2':
|
|
467
|
-
|
|
652
|
+
// TODO: HANDLE PARTICIPANT_PN
|
|
653
|
+
handleGroupNotification(node, child, result);
|
|
468
654
|
break;
|
|
469
655
|
case 'mediaretry':
|
|
470
|
-
const event =
|
|
656
|
+
const event = decodeMediaRetryNode(node);
|
|
471
657
|
ev.emit('messages.media-update', [event]);
|
|
472
658
|
break;
|
|
473
659
|
case 'encrypt':
|
|
474
660
|
await handleEncryptNotification(node);
|
|
475
661
|
break;
|
|
476
662
|
case 'devices':
|
|
477
|
-
const devices =
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
663
|
+
const devices = getBinaryNodeChildren(child, 'device');
|
|
664
|
+
if (areJidsSameUser(child.attrs.jid, authState.creds.me.id) ||
|
|
665
|
+
areJidsSameUser(child.attrs.lid, authState.creds.me.lid)) {
|
|
666
|
+
const deviceData = devices.map(d => ({ id: d.attrs.jid, lid: d.attrs.lid }));
|
|
667
|
+
logger.info({ deviceData }, 'my own devices changed');
|
|
481
668
|
}
|
|
669
|
+
//TODO: drop a new event, add hashes
|
|
482
670
|
break;
|
|
483
671
|
case 'server_sync':
|
|
484
|
-
const update =
|
|
672
|
+
const update = getBinaryNodeChild(node, 'collection');
|
|
485
673
|
if (update) {
|
|
486
674
|
const name = update.attrs.name;
|
|
487
|
-
|
|
488
|
-
// Sync runs concurrently with real-time message delivery
|
|
489
|
-
resyncAppState([name], false).catch(e => logger.warn({ e, name }, 'background server_sync resync failed'));
|
|
675
|
+
await resyncAppState([name], false);
|
|
490
676
|
}
|
|
491
677
|
break;
|
|
492
678
|
case 'picture':
|
|
493
|
-
const setPicture =
|
|
494
|
-
const delPicture =
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
679
|
+
const setPicture = getBinaryNodeChild(node, 'set');
|
|
680
|
+
const delPicture = getBinaryNodeChild(node, 'delete');
|
|
681
|
+
// TODO: WAJIDHASH stuff proper support inhouse
|
|
682
|
+
ev.emit('contacts.update', [
|
|
683
|
+
{
|
|
684
|
+
id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
|
|
685
|
+
imgUrl: setPicture ? 'changed' : 'removed'
|
|
686
|
+
}
|
|
687
|
+
]);
|
|
688
|
+
if (isJidGroup(from)) {
|
|
500
689
|
const node = setPicture || delPicture;
|
|
501
|
-
result.messageStubType =
|
|
690
|
+
result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON;
|
|
502
691
|
if (setPicture) {
|
|
503
692
|
result.messageStubParameters = [setPicture.attrs.id];
|
|
504
693
|
}
|
|
505
|
-
result.participant = node
|
|
694
|
+
result.participant = node?.attrs.author;
|
|
506
695
|
result.key = {
|
|
507
|
-
...result.key || {},
|
|
508
|
-
participant: setPicture
|
|
696
|
+
...(result.key || {}),
|
|
697
|
+
participant: setPicture?.attrs.author
|
|
509
698
|
};
|
|
510
699
|
}
|
|
511
700
|
break;
|
|
@@ -519,44 +708,48 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
519
708
|
...authState.creds.accountSettings,
|
|
520
709
|
defaultDisappearingMode: {
|
|
521
710
|
ephemeralExpiration: newDuration,
|
|
522
|
-
ephemeralSettingTimestamp: timestamp
|
|
523
|
-
}
|
|
711
|
+
ephemeralSettingTimestamp: timestamp
|
|
712
|
+
}
|
|
524
713
|
}
|
|
525
714
|
});
|
|
526
715
|
}
|
|
527
716
|
else if (child.tag === 'blocklist') {
|
|
528
|
-
const blocklists =
|
|
717
|
+
const blocklists = getBinaryNodeChildren(child, 'item');
|
|
529
718
|
for (const { attrs } of blocklists) {
|
|
530
719
|
const blocklist = [attrs.jid];
|
|
531
|
-
const type =
|
|
720
|
+
const type = attrs.action === 'block' ? 'add' : 'remove';
|
|
532
721
|
ev.emit('blocklist.update', { blocklist, type });
|
|
533
722
|
}
|
|
534
723
|
}
|
|
535
724
|
break;
|
|
536
725
|
case 'link_code_companion_reg':
|
|
537
|
-
const linkCodeCompanionReg =
|
|
538
|
-
const ref = toRequiredBuffer(
|
|
539
|
-
const primaryIdentityPublicKey = toRequiredBuffer(
|
|
540
|
-
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
|
|
726
|
+
const linkCodeCompanionReg = getBinaryNodeChild(node, 'link_code_companion_reg');
|
|
727
|
+
const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'));
|
|
728
|
+
const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'));
|
|
729
|
+
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'));
|
|
541
730
|
const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped);
|
|
542
|
-
const companionSharedKey =
|
|
543
|
-
const random =
|
|
544
|
-
const linkCodeSalt =
|
|
545
|
-
const linkCodePairingExpanded =
|
|
731
|
+
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey);
|
|
732
|
+
const random = randomBytes(32);
|
|
733
|
+
const linkCodeSalt = randomBytes(32);
|
|
734
|
+
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, {
|
|
546
735
|
salt: linkCodeSalt,
|
|
547
736
|
info: 'link_code_pairing_key_bundle_encryption_key'
|
|
548
737
|
});
|
|
549
|
-
const encryptPayload = Buffer.concat([
|
|
550
|
-
|
|
551
|
-
|
|
738
|
+
const encryptPayload = Buffer.concat([
|
|
739
|
+
Buffer.from(authState.creds.signedIdentityKey.public),
|
|
740
|
+
primaryIdentityPublicKey,
|
|
741
|
+
random
|
|
742
|
+
]);
|
|
743
|
+
const encryptIv = randomBytes(12);
|
|
744
|
+
const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0));
|
|
552
745
|
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted]);
|
|
553
|
-
const identitySharedKey =
|
|
746
|
+
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey);
|
|
554
747
|
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random]);
|
|
555
|
-
authState.creds.advSecretKey = (
|
|
748
|
+
authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64');
|
|
556
749
|
await query({
|
|
557
750
|
tag: 'iq',
|
|
558
751
|
attrs: {
|
|
559
|
-
to:
|
|
752
|
+
to: S_WHATSAPP_NET,
|
|
560
753
|
type: 'set',
|
|
561
754
|
id: sock.generateMessageTag(),
|
|
562
755
|
xmlns: 'md'
|
|
@@ -566,7 +759,7 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
566
759
|
tag: 'link_code_companion_reg',
|
|
567
760
|
attrs: {
|
|
568
761
|
jid: authState.creds.me.id,
|
|
569
|
-
stage: 'companion_finish'
|
|
762
|
+
stage: 'companion_finish'
|
|
570
763
|
},
|
|
571
764
|
content: [
|
|
572
765
|
{
|
|
@@ -590,53 +783,125 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
590
783
|
});
|
|
591
784
|
authState.creds.registered = true;
|
|
592
785
|
ev.emit('creds.update', authState.creds);
|
|
786
|
+
break;
|
|
787
|
+
case 'privacy_token':
|
|
788
|
+
await handlePrivacyTokenNotification(node);
|
|
789
|
+
break;
|
|
593
790
|
}
|
|
594
791
|
if (Object.keys(result).length) {
|
|
595
792
|
return result;
|
|
596
793
|
}
|
|
597
794
|
};
|
|
795
|
+
const handlePrivacyTokenNotification = async (node) => {
|
|
796
|
+
const tokensNode = getBinaryNodeChild(node, 'tokens');
|
|
797
|
+
const from = jidNormalizedUser(node.attrs.from);
|
|
798
|
+
if (!tokensNode)
|
|
799
|
+
return;
|
|
800
|
+
const tokenNodes = getBinaryNodeChildren(tokensNode, 'token');
|
|
801
|
+
for (const tokenNode of tokenNodes) {
|
|
802
|
+
const { attrs, content } = tokenNode;
|
|
803
|
+
const type = attrs.type;
|
|
804
|
+
const timestamp = attrs.t;
|
|
805
|
+
if (type === 'trusted_contact' && content instanceof Buffer) {
|
|
806
|
+
logger.debug({
|
|
807
|
+
from,
|
|
808
|
+
timestamp,
|
|
809
|
+
tcToken: content
|
|
810
|
+
}, 'received trusted contact token');
|
|
811
|
+
await authState.keys.set({
|
|
812
|
+
tctoken: { [from]: { token: content, timestamp } }
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
598
817
|
async function decipherLinkPublicKey(data) {
|
|
599
818
|
const buffer = toRequiredBuffer(data);
|
|
600
819
|
const salt = buffer.slice(0, 32);
|
|
601
|
-
const secretKey = await
|
|
820
|
+
const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt);
|
|
602
821
|
const iv = buffer.slice(32, 48);
|
|
603
822
|
const payload = buffer.slice(48, 80);
|
|
604
|
-
return
|
|
823
|
+
return aesDecryptCTR(payload, secretKey, iv);
|
|
605
824
|
}
|
|
606
825
|
function toRequiredBuffer(data) {
|
|
607
826
|
if (data === undefined) {
|
|
608
|
-
throw new
|
|
827
|
+
throw new Boom('Invalid buffer', { statusCode: 400 });
|
|
609
828
|
}
|
|
610
829
|
return data instanceof Buffer ? data : Buffer.from(data);
|
|
611
830
|
}
|
|
612
|
-
const willSendMessageAgain = (id, participant) => {
|
|
831
|
+
const willSendMessageAgain = async (id, participant) => {
|
|
613
832
|
const key = `${id}:${participant}`;
|
|
614
|
-
const retryCount = msgRetryCache.get(key) || 0;
|
|
833
|
+
const retryCount = (await msgRetryCache.get(key)) || 0;
|
|
615
834
|
return retryCount < maxMsgRetryCount;
|
|
616
835
|
};
|
|
617
|
-
const updateSendMessageAgainCount = (id, participant) => {
|
|
836
|
+
const updateSendMessageAgainCount = async (id, participant) => {
|
|
618
837
|
const key = `${id}:${participant}`;
|
|
619
|
-
const newValue = (msgRetryCache.get(key) || 0) + 1;
|
|
620
|
-
msgRetryCache.set(key, newValue);
|
|
838
|
+
const newValue = ((await msgRetryCache.get(key)) || 0) + 1;
|
|
839
|
+
await msgRetryCache.set(key, newValue);
|
|
621
840
|
};
|
|
622
841
|
const sendMessagesAgain = async (key, ids, retryNode) => {
|
|
623
|
-
var _a;
|
|
624
|
-
// todo: implement a cache to store the last 256 sent messages (copy whatsmeow)
|
|
625
|
-
const msgs = await Promise.all(ids.map(id => getMessage({ ...key, id })));
|
|
626
842
|
const remoteJid = key.remoteJid;
|
|
627
843
|
const participant = key.participant || remoteJid;
|
|
844
|
+
const retryCount = +retryNode.attrs.count || 1;
|
|
845
|
+
// Try to get messages from cache first, then fallback to getMessage
|
|
846
|
+
const msgs = [];
|
|
847
|
+
for (const id of ids) {
|
|
848
|
+
let msg;
|
|
849
|
+
// Try to get from retry cache first if enabled
|
|
850
|
+
if (messageRetryManager) {
|
|
851
|
+
const cachedMsg = messageRetryManager.getRecentMessage(remoteJid, id);
|
|
852
|
+
if (cachedMsg) {
|
|
853
|
+
msg = cachedMsg.message;
|
|
854
|
+
logger.debug({ jid: remoteJid, id }, 'found message in retry cache');
|
|
855
|
+
// Mark retry as successful since we found the message
|
|
856
|
+
messageRetryManager.markRetrySuccess(id);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// Fallback to getMessage if not found in cache
|
|
860
|
+
if (!msg) {
|
|
861
|
+
msg = await getMessage({ ...key, id });
|
|
862
|
+
if (msg) {
|
|
863
|
+
logger.debug({ jid: remoteJid, id }, 'found message via getMessage');
|
|
864
|
+
// Also mark as successful if found via getMessage
|
|
865
|
+
if (messageRetryManager) {
|
|
866
|
+
messageRetryManager.markRetrySuccess(id);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
msgs.push(msg);
|
|
871
|
+
}
|
|
628
872
|
// if it's the primary jid sending the request
|
|
629
873
|
// just re-send the message to everyone
|
|
630
874
|
// prevents the first message decryption failure
|
|
631
|
-
const sendToAll = !
|
|
875
|
+
const sendToAll = !jidDecode(participant)?.device;
|
|
876
|
+
// Check if we should recreate session for this retry
|
|
877
|
+
let shouldRecreateSession = false;
|
|
878
|
+
let recreateReason = '';
|
|
879
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
880
|
+
try {
|
|
881
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(participant);
|
|
882
|
+
const hasSession = await signalRepository.validateSession(participant);
|
|
883
|
+
const result = messageRetryManager.shouldRecreateSession(participant, hasSession.exists);
|
|
884
|
+
shouldRecreateSession = result.recreate;
|
|
885
|
+
recreateReason = result.reason;
|
|
886
|
+
if (shouldRecreateSession) {
|
|
887
|
+
logger.debug({ participant, retryCount, reason: recreateReason }, 'recreating session for outgoing retry');
|
|
888
|
+
await authState.keys.set({ session: { [sessionId]: null } });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry');
|
|
893
|
+
}
|
|
894
|
+
}
|
|
632
895
|
await assertSessions([participant], true);
|
|
633
|
-
if (
|
|
896
|
+
if (isJidGroup(remoteJid)) {
|
|
634
897
|
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
|
|
635
898
|
}
|
|
636
|
-
logger.debug({ participant, sendToAll }, 'forced new session for retry recp');
|
|
899
|
+
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason }, 'forced new session for retry recp');
|
|
637
900
|
for (const [i, msg] of msgs.entries()) {
|
|
638
|
-
if (
|
|
639
|
-
|
|
901
|
+
if (!ids[i])
|
|
902
|
+
continue;
|
|
903
|
+
if (msg && (await willSendMessageAgain(ids[i], participant))) {
|
|
904
|
+
await updateSendMessageAgainCount(ids[i], participant);
|
|
640
905
|
const msgRelayOpts = { messageId: ids[i] };
|
|
641
906
|
if (sendToAll) {
|
|
642
907
|
msgRelayOpts.useUserDevicesCache = false;
|
|
@@ -655,11 +920,10 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
655
920
|
}
|
|
656
921
|
};
|
|
657
922
|
const handleReceipt = async (node) => {
|
|
658
|
-
var _a, _b;
|
|
659
923
|
const { attrs, content } = node;
|
|
660
924
|
const isLid = attrs.from.includes('lid');
|
|
661
|
-
const isNodeFromMe =
|
|
662
|
-
const remoteJid = !isNodeFromMe ||
|
|
925
|
+
const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id);
|
|
926
|
+
const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient;
|
|
663
927
|
const fromMe = !attrs.recipient || ((attrs.type === 'retry' || attrs.type === 'sender') && isNodeFromMe);
|
|
664
928
|
const key = {
|
|
665
929
|
remoteJid,
|
|
@@ -667,33 +931,31 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
667
931
|
fromMe,
|
|
668
932
|
participant: attrs.participant
|
|
669
933
|
};
|
|
670
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !==
|
|
934
|
+
if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
|
|
671
935
|
logger.debug({ remoteJid }, 'ignoring receipt from jid');
|
|
672
936
|
await sendMessageAck(node);
|
|
673
937
|
return;
|
|
674
938
|
}
|
|
675
939
|
const ids = [attrs.id];
|
|
676
940
|
if (Array.isArray(content)) {
|
|
677
|
-
const items =
|
|
941
|
+
const items = getBinaryNodeChildren(content[0], 'item');
|
|
678
942
|
ids.push(...items.map(i => i.attrs.id));
|
|
679
943
|
}
|
|
680
944
|
try {
|
|
681
945
|
await Promise.all([
|
|
682
|
-
|
|
683
|
-
const status =
|
|
946
|
+
receiptMutex.mutex(async () => {
|
|
947
|
+
const status = getStatusFromReceiptType(attrs.type);
|
|
684
948
|
if (typeof status !== 'undefined' &&
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
!isNodeFromMe)) {
|
|
690
|
-
if ((0, WABinary_1.isJidGroup)(remoteJid) || (0, WABinary_1.isJidStatusBroadcast)(remoteJid)) {
|
|
949
|
+
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
|
950
|
+
// or another device of ours has read some messages
|
|
951
|
+
(status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
|
|
952
|
+
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
|
|
691
953
|
if (attrs.participant) {
|
|
692
|
-
const updateKey = status ===
|
|
954
|
+
const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp';
|
|
693
955
|
ev.emit('message-receipt.update', ids.map(id => ({
|
|
694
956
|
key: { ...key, id },
|
|
695
957
|
receipt: {
|
|
696
|
-
userJid:
|
|
958
|
+
userJid: jidNormalizedUser(attrs.participant),
|
|
697
959
|
[updateKey]: +attrs.t
|
|
698
960
|
}
|
|
699
961
|
})));
|
|
@@ -702,22 +964,23 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
702
964
|
else {
|
|
703
965
|
ev.emit('messages.update', ids.map(id => ({
|
|
704
966
|
key: { ...key, id },
|
|
705
|
-
update: { status }
|
|
967
|
+
update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) }
|
|
706
968
|
})));
|
|
707
969
|
}
|
|
708
970
|
}
|
|
709
971
|
if (attrs.type === 'retry') {
|
|
710
972
|
// correctly set who is asking for the retry
|
|
711
973
|
key.participant = key.participant || attrs.from;
|
|
712
|
-
const retryNode =
|
|
713
|
-
if (willSendMessageAgain(ids[0], key.participant)) {
|
|
974
|
+
const retryNode = getBinaryNodeChild(node, 'retry');
|
|
975
|
+
if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
|
|
714
976
|
if (key.fromMe) {
|
|
715
977
|
try {
|
|
978
|
+
await updateSendMessageAgainCount(ids[0], key.participant);
|
|
716
979
|
logger.debug({ attrs, key }, 'recv retry request');
|
|
717
980
|
await sendMessagesAgain(key, ids, retryNode);
|
|
718
981
|
}
|
|
719
982
|
catch (error) {
|
|
720
|
-
logger.error({ key, ids, trace: error.stack }, 'error in sending message again');
|
|
983
|
+
logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again');
|
|
721
984
|
}
|
|
722
985
|
}
|
|
723
986
|
else {
|
|
@@ -737,28 +1000,30 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
737
1000
|
};
|
|
738
1001
|
const handleNotification = async (node) => {
|
|
739
1002
|
const remoteJid = node.attrs.from;
|
|
740
|
-
if (shouldIgnoreJid(remoteJid) && remoteJid !==
|
|
1003
|
+
if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) {
|
|
741
1004
|
logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification');
|
|
742
1005
|
await sendMessageAck(node);
|
|
743
1006
|
return;
|
|
744
1007
|
}
|
|
745
1008
|
try {
|
|
746
1009
|
await Promise.all([
|
|
747
|
-
|
|
748
|
-
var _a;
|
|
1010
|
+
notificationMutex.mutex(async () => {
|
|
749
1011
|
const msg = await processNotification(node);
|
|
750
1012
|
if (msg) {
|
|
751
|
-
const fromMe =
|
|
1013
|
+
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id);
|
|
1014
|
+
const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node);
|
|
752
1015
|
msg.key = {
|
|
753
1016
|
remoteJid,
|
|
754
1017
|
fromMe,
|
|
755
1018
|
participant: node.attrs.participant,
|
|
1019
|
+
participantAlt,
|
|
1020
|
+
addressingMode,
|
|
756
1021
|
id: node.attrs.id,
|
|
757
1022
|
...(msg.key || {})
|
|
758
1023
|
};
|
|
759
|
-
|
|
1024
|
+
msg.participant ?? (msg.participant = node.attrs.participant);
|
|
760
1025
|
msg.messageTimestamp = +node.attrs.t;
|
|
761
|
-
const fullMsg =
|
|
1026
|
+
const fullMsg = proto.WebMessageInfo.fromObject(msg);
|
|
762
1027
|
await upsertMessage(fullMsg, 'append');
|
|
763
1028
|
}
|
|
764
1029
|
})
|
|
@@ -769,222 +1034,254 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
769
1034
|
}
|
|
770
1035
|
};
|
|
771
1036
|
const handleMessage = async (node) => {
|
|
772
|
-
|
|
773
|
-
if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== '@s.whatsapp.net') {
|
|
1037
|
+
if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) {
|
|
774
1038
|
logger.debug({ key: node.attrs.key }, 'ignored message');
|
|
775
|
-
await sendMessageAck(node);
|
|
1039
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
776
1040
|
return;
|
|
777
1041
|
}
|
|
778
|
-
const encNode =
|
|
1042
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
779
1043
|
// TODO: temporary fix for crashes and issues resulting of failed msmsg decryption
|
|
780
|
-
if (encNode
|
|
1044
|
+
if (encNode?.attrs.type === 'msmsg') {
|
|
781
1045
|
logger.debug({ key: node.attrs.key }, 'ignored msmsg');
|
|
782
|
-
await sendMessageAck(node);
|
|
1046
|
+
await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
|
|
783
1047
|
return;
|
|
784
1048
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1049
|
+
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
|
|
1050
|
+
const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
|
|
1051
|
+
// store new mappings we didn't have before
|
|
1052
|
+
if (!!alt) {
|
|
1053
|
+
const altServer = jidDecode(alt)?.server;
|
|
1054
|
+
const primaryJid = msg.key.participant || msg.key.remoteJid;
|
|
1055
|
+
if (altServer === 'lid') {
|
|
1056
|
+
if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
|
|
1057
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
|
|
1058
|
+
await signalRepository.migrateSession(primaryJid, alt);
|
|
1059
|
+
}
|
|
792
1060
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (placeholderResendCache.get(node.attrs.id)) {
|
|
797
|
-
placeholderResendCache.del(node.attrs.id);
|
|
1061
|
+
else {
|
|
1062
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
|
|
1063
|
+
await signalRepository.migrateSession(alt, primaryJid);
|
|
798
1064
|
}
|
|
799
1065
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1066
|
+
if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
|
|
1067
|
+
messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
|
|
1068
|
+
logger.debug({
|
|
1069
|
+
jid: msg.key.remoteJid,
|
|
1070
|
+
id: msg.key.id
|
|
1071
|
+
}, 'Added message to recent cache for retry receipts');
|
|
806
1072
|
}
|
|
807
1073
|
try {
|
|
808
|
-
await
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1074
|
+
await messageMutex.mutex(async () => {
|
|
1075
|
+
await decrypt();
|
|
1076
|
+
// message failed to decrypt
|
|
1077
|
+
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
1078
|
+
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
|
|
1079
|
+
return sendMessageAck(node, NACK_REASONS.ParsingError);
|
|
1080
|
+
}
|
|
1081
|
+
if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
1082
|
+
// Message arrived without encryption (e.g. CTWA ads messages).
|
|
1083
|
+
// Check if this is eligible for placeholder resend (matching WA Web filters).
|
|
1084
|
+
const unavailableNode = getBinaryNodeChild(node, 'unavailable');
|
|
1085
|
+
const unavailableType = unavailableNode?.attrs?.type;
|
|
1086
|
+
if (unavailableType === 'bot_unavailable_fanout' ||
|
|
1087
|
+
unavailableType === 'hosted_unavailable_fanout' ||
|
|
1088
|
+
unavailableType === 'view_once_unavailable_fanout') {
|
|
1089
|
+
logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
|
|
1090
|
+
return sendMessageAck(node);
|
|
816
1091
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1092
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1093
|
+
if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
|
|
1094
|
+
logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
|
|
1095
|
+
return sendMessageAck(node);
|
|
1096
|
+
}
|
|
1097
|
+
// Request the real content from the phone via placeholder resend PDO.
|
|
1098
|
+
// Upsert the CIPHERTEXT stub as a placeholder (like WA Web's processPlaceholderMsg),
|
|
1099
|
+
// and store the requestId in stubParameters[1] so users can correlate
|
|
1100
|
+
// with the incoming PDO response event.
|
|
1101
|
+
const cleanKey = {
|
|
1102
|
+
remoteJid: msg.key.remoteJid,
|
|
1103
|
+
fromMe: msg.key.fromMe,
|
|
1104
|
+
id: msg.key.id,
|
|
1105
|
+
participant: msg.key.participant
|
|
1106
|
+
};
|
|
1107
|
+
// Cache the original message metadata so the PDO response handler
|
|
1108
|
+
// can preserve key fields (LID details etc.) that the phone may omit
|
|
1109
|
+
const msgData = {
|
|
1110
|
+
key: msg.key,
|
|
1111
|
+
messageTimestamp: msg.messageTimestamp,
|
|
1112
|
+
pushName: msg.pushName,
|
|
1113
|
+
participant: msg.participant,
|
|
1114
|
+
verifiedBizName: msg.verifiedBizName
|
|
1115
|
+
};
|
|
1116
|
+
requestPlaceholderResend(cleanKey, msgData)
|
|
1117
|
+
.then(requestId => {
|
|
1118
|
+
if (requestId && requestId !== 'RESOLVED') {
|
|
1119
|
+
logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message');
|
|
1120
|
+
ev.emit('messages.update', [
|
|
1121
|
+
{
|
|
1122
|
+
key: msg.key,
|
|
1123
|
+
update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] }
|
|
1124
|
+
}
|
|
1125
|
+
]);
|
|
1126
|
+
}
|
|
1127
|
+
})
|
|
1128
|
+
.catch(err => {
|
|
1129
|
+
logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
|
|
1130
|
+
});
|
|
1131
|
+
await sendMessageAck(node);
|
|
1132
|
+
// Don't return — fall through to upsertMessage so the stub is emitted
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
// Skip retry for expired status messages (>24h old)
|
|
1136
|
+
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
1137
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
|
|
1138
|
+
if (messageAge > STATUS_EXPIRY_SECONDS) {
|
|
1139
|
+
logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
|
|
1140
|
+
return sendMessageAck(node);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
const errorMessage = msg?.messageStubParameters?.[0] || '';
|
|
1144
|
+
const isPreKeyError = errorMessage.includes('PreKey');
|
|
1145
|
+
logger.debug(`[handleMessage] Attempting retry request for failed decryption`);
|
|
1146
|
+
// Handle both pre-key and normal retries in single mutex
|
|
1147
|
+
await retryMutex.mutex(async () => {
|
|
1148
|
+
try {
|
|
1149
|
+
if (!ws.isOpen) {
|
|
1150
|
+
logger.debug({ node }, 'Connection closed, skipping retry');
|
|
820
1151
|
return;
|
|
821
1152
|
}
|
|
822
|
-
|
|
1153
|
+
// Handle pre-key errors with upload and delay
|
|
1154
|
+
if (isPreKeyError) {
|
|
1155
|
+
logger.info({ error: errorMessage }, 'PreKey error detected, uploading and retrying');
|
|
1156
|
+
try {
|
|
1157
|
+
logger.debug('Uploading pre-keys for error recovery');
|
|
1158
|
+
await uploadPreKeys(5);
|
|
1159
|
+
logger.debug('Waiting for server to process new pre-keys');
|
|
1160
|
+
await delay(1000);
|
|
1161
|
+
}
|
|
1162
|
+
catch (uploadErr) {
|
|
1163
|
+
logger.error({ uploadErr }, 'Pre-key upload failed, proceeding with retry anyway');
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
823
1167
|
await sendRetryRequest(node, !encNode);
|
|
824
1168
|
if (retryRequestDelayMs) {
|
|
825
|
-
await
|
|
1169
|
+
await delay(retryRequestDelayMs);
|
|
826
1170
|
}
|
|
827
1171
|
}
|
|
828
|
-
|
|
829
|
-
logger.
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
logger.error({ err, isPreKeyError }, 'Failed to handle retry, attempting basic retry');
|
|
1174
|
+
// Still attempt retry even if pre-key upload failed
|
|
1175
|
+
try {
|
|
1176
|
+
const encNode = getBinaryNodeChild(node, 'enc');
|
|
1177
|
+
await sendRetryRequest(node, !encNode);
|
|
1178
|
+
}
|
|
1179
|
+
catch (retryErr) {
|
|
1180
|
+
logger.error({ retryErr }, 'Failed to send retry after error handling');
|
|
1181
|
+
}
|
|
830
1182
|
}
|
|
1183
|
+
await sendMessageAck(node, NACK_REASONS.UnhandledError);
|
|
831
1184
|
});
|
|
832
1185
|
}
|
|
833
|
-
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
if (messageRetryManager && msg.key.id) {
|
|
1189
|
+
messageRetryManager.cancelPendingPhoneRequest(msg.key.id);
|
|
1190
|
+
}
|
|
1191
|
+
const isNewsletter = isJidNewsletter(msg.key.remoteJid);
|
|
1192
|
+
if (!isNewsletter) {
|
|
834
1193
|
// no type in the receipt => message delivered
|
|
835
1194
|
let type = undefined;
|
|
836
|
-
if ((_b = msg.key.participant) === null || _b === void 0 ? void 0 : _b.endsWith('@lid')) {
|
|
837
|
-
msg.key.participant = node.attrs.participant_pn || authState.creds.me.id;
|
|
838
|
-
}
|
|
839
|
-
if ((0, WABinary_1.isJidGroup)(msg.key.remoteJid) && ((_f = (_e = (_d = (_c = msg.message) === null || _c === void 0 ? void 0 : _c.extendedTextMessage) === null || _d === void 0 ? void 0 : _d.contextInfo) === null || _e === void 0 ? void 0 : _e.participant) === null || _f === void 0 ? void 0 : _f.endsWith('@lid'))) {
|
|
840
|
-
if (msg.message.extendedTextMessage.contextInfo) {
|
|
841
|
-
const metadata = await groupMetadata(msg.key.remoteJid);
|
|
842
|
-
const sender = msg.message.extendedTextMessage.contextInfo.participant;
|
|
843
|
-
const found = metadata.participants.find(p => p.id === sender);
|
|
844
|
-
msg.message.extendedTextMessage.contextInfo.participant = (found === null || found === void 0 ? void 0 : found.jid) || sender;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
if (!(0, WABinary_1.isJidGroup)(msg.key.remoteJid) && (0, WABinary_1.isLidUser)(msg.key.remoteJid)) {
|
|
848
|
-
msg.key.remoteJid = node.attrs.sender_pn || node.attrs.peer_recipient_pn;
|
|
849
|
-
}
|
|
850
1195
|
let participant = msg.key.participant;
|
|
851
|
-
if (category === 'peer') {
|
|
1196
|
+
if (category === 'peer') {
|
|
1197
|
+
// special peer message
|
|
852
1198
|
type = 'peer_msg';
|
|
853
1199
|
}
|
|
854
|
-
else if (msg.key.fromMe) {
|
|
1200
|
+
else if (msg.key.fromMe) {
|
|
1201
|
+
// message was sent by us from a different device
|
|
855
1202
|
type = 'sender';
|
|
856
1203
|
// need to specially handle this case
|
|
857
|
-
if ((
|
|
858
|
-
participant = author;
|
|
1204
|
+
if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) {
|
|
1205
|
+
participant = author; // TODO: investigate sending receipts to LIDs and not PNs
|
|
859
1206
|
}
|
|
860
1207
|
}
|
|
861
1208
|
else if (!sendActiveReceipts) {
|
|
862
1209
|
type = 'inactive';
|
|
863
1210
|
}
|
|
864
|
-
|
|
865
|
-
//
|
|
866
|
-
const isAnyHistoryMsg =
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
: Promise.resolve()
|
|
872
|
-
]);
|
|
1211
|
+
await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
|
|
1212
|
+
// send ack for history message
|
|
1213
|
+
const isAnyHistoryMsg = getHistoryMsg(msg.message);
|
|
1214
|
+
if (isAnyHistoryMsg) {
|
|
1215
|
+
const jid = jidNormalizedUser(msg.key.remoteJid);
|
|
1216
|
+
await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync'); // TODO: investigate
|
|
1217
|
+
}
|
|
873
1218
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1219
|
+
else {
|
|
1220
|
+
await sendMessageAck(node);
|
|
1221
|
+
logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid);
|
|
1225
|
+
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
|
|
1226
|
+
});
|
|
879
1227
|
}
|
|
880
1228
|
catch (error) {
|
|
881
|
-
logger.error({ error, node }, 'error in handling message');
|
|
1229
|
+
logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
|
|
882
1230
|
}
|
|
883
1231
|
};
|
|
884
|
-
const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
|
|
885
|
-
var _a;
|
|
886
|
-
if (!((_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
887
|
-
throw new boom_1.Boom('Not authenticated');
|
|
888
|
-
}
|
|
889
|
-
const pdoMessage = {
|
|
890
|
-
historySyncOnDemandRequest: {
|
|
891
|
-
chatJid: oldestMsgKey.remoteJid,
|
|
892
|
-
oldestMsgFromMe: oldestMsgKey.fromMe,
|
|
893
|
-
oldestMsgId: oldestMsgKey.id,
|
|
894
|
-
oldestMsgTimestampMs: oldestMsgTimestamp,
|
|
895
|
-
onDemandMsgCount: count
|
|
896
|
-
},
|
|
897
|
-
peerDataOperationRequestType: WAProto_1.proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND
|
|
898
|
-
};
|
|
899
|
-
return sendPeerDataOperationMessage(pdoMessage);
|
|
900
|
-
};
|
|
901
|
-
const requestPlaceholderResend = async (messageKey) => {
|
|
902
|
-
var _a;
|
|
903
|
-
if (!((_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
904
|
-
throw new boom_1.Boom('Not authenticated');
|
|
905
|
-
}
|
|
906
|
-
if (placeholderResendCache.get(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id)) {
|
|
907
|
-
logger.debug({ messageKey }, 'already requested resend');
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
else {
|
|
911
|
-
placeholderResendCache.set(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id, true);
|
|
912
|
-
}
|
|
913
|
-
await (0, Utils_1.delay)(800); // reduced from 5000ms — wait briefly for message to arrive before requesting resend
|
|
914
|
-
if (!placeholderResendCache.get(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id)) {
|
|
915
|
-
logger.debug({ messageKey }, 'message received while resend requested');
|
|
916
|
-
return 'RESOLVED';
|
|
917
|
-
}
|
|
918
|
-
const pdoMessage = {
|
|
919
|
-
placeholderMessageResendRequest: [{
|
|
920
|
-
messageKey
|
|
921
|
-
}],
|
|
922
|
-
peerDataOperationRequestType: WAProto_1.proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
|
|
923
|
-
};
|
|
924
|
-
setTimeout(() => {
|
|
925
|
-
if (placeholderResendCache.get(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id)) {
|
|
926
|
-
logger.debug({ messageKey }, 'PDO message without response after 15 seconds. Phone possibly offline');
|
|
927
|
-
placeholderResendCache.del(messageKey === null || messageKey === void 0 ? void 0 : messageKey.id);
|
|
928
|
-
}
|
|
929
|
-
}, 15000);
|
|
930
|
-
return sendPeerDataOperationMessage(pdoMessage);
|
|
931
|
-
};
|
|
932
1232
|
const handleCall = async (node) => {
|
|
933
1233
|
const { attrs } = node;
|
|
934
|
-
const [infoChild] =
|
|
1234
|
+
const [infoChild] = getAllBinaryNodeChildren(node);
|
|
1235
|
+
const status = getCallStatusFromNode(infoChild);
|
|
1236
|
+
if (!infoChild) {
|
|
1237
|
+
throw new Boom('Missing call info in call node');
|
|
1238
|
+
}
|
|
935
1239
|
const callId = infoChild.attrs['call-id'];
|
|
936
1240
|
const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
|
|
937
|
-
const status = (0, Utils_1.getCallStatusFromNode)(infoChild);
|
|
938
1241
|
const call = {
|
|
939
1242
|
chatId: attrs.from,
|
|
940
1243
|
from,
|
|
1244
|
+
callerPn: infoChild.attrs['caller_pn'],
|
|
941
1245
|
id: callId,
|
|
942
1246
|
date: new Date(+attrs.t * 1000),
|
|
943
1247
|
offline: !!attrs.offline,
|
|
944
|
-
status
|
|
1248
|
+
status
|
|
945
1249
|
};
|
|
946
1250
|
if (status === 'offer') {
|
|
947
|
-
call.isVideo = !!
|
|
1251
|
+
call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
|
|
948
1252
|
call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
|
|
949
1253
|
call.groupJid = infoChild.attrs['group-jid'];
|
|
950
|
-
callOfferCache.set(call.id, call);
|
|
1254
|
+
await callOfferCache.set(call.id, call);
|
|
951
1255
|
}
|
|
952
|
-
const existingCall = callOfferCache.get(call.id);
|
|
1256
|
+
const existingCall = await callOfferCache.get(call.id);
|
|
953
1257
|
// use existing call info to populate this event
|
|
954
1258
|
if (existingCall) {
|
|
955
1259
|
call.isVideo = existingCall.isVideo;
|
|
956
1260
|
call.isGroup = existingCall.isGroup;
|
|
1261
|
+
call.callerPn = call.callerPn || existingCall.callerPn;
|
|
957
1262
|
}
|
|
958
1263
|
// delete data once call has ended
|
|
959
1264
|
if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
|
|
960
|
-
callOfferCache.del(call.id);
|
|
1265
|
+
await callOfferCache.del(call.id);
|
|
961
1266
|
}
|
|
962
1267
|
ev.emit('call', [call]);
|
|
963
1268
|
await sendMessageAck(node);
|
|
964
1269
|
};
|
|
965
1270
|
const handleBadAck = async ({ attrs }) => {
|
|
966
|
-
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
//
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if (msg) {
|
|
981
|
-
await relayMessage(key.remoteJid, msg, { messageId: key.id, useUserDevicesCache: false });
|
|
982
|
-
msgRetryCache.set(cacheKey, retryCount + 1);
|
|
983
|
-
}
|
|
984
|
-
else {
|
|
985
|
-
logger.warn({ attrs }, 'could not send message again, as it was not found');
|
|
986
|
-
}
|
|
987
|
-
}
|
|
1271
|
+
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
|
|
1272
|
+
// WARNING: REFRAIN FROM ENABLING THIS FOR NOW. IT WILL CAUSE A LOOP
|
|
1273
|
+
// // current hypothesis is that if pash is sent in the ack
|
|
1274
|
+
// // it means -- the message hasn't reached all devices yet
|
|
1275
|
+
// // we'll retry sending the message here
|
|
1276
|
+
// if(attrs.phash) {
|
|
1277
|
+
// logger.info({ attrs }, 'received phash in ack, resending message...')
|
|
1278
|
+
// const msg = await getMessage(key)
|
|
1279
|
+
// if(msg) {
|
|
1280
|
+
// await relayMessage(key.remoteJid!, msg, { messageId: key.id!, useUserDevicesCache: false })
|
|
1281
|
+
// } else {
|
|
1282
|
+
// logger.warn({ attrs }, 'could not send message again, as it was not found')
|
|
1283
|
+
// }
|
|
1284
|
+
// }
|
|
988
1285
|
// error in acknowledgement,
|
|
989
1286
|
// device could not display the message
|
|
990
1287
|
if (attrs.error) {
|
|
@@ -993,24 +1290,39 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
993
1290
|
{
|
|
994
1291
|
key,
|
|
995
1292
|
update: {
|
|
996
|
-
status:
|
|
997
|
-
messageStubParameters: [
|
|
998
|
-
attrs.error
|
|
999
|
-
]
|
|
1293
|
+
status: WAMessageStatus.ERROR,
|
|
1294
|
+
messageStubParameters: [attrs.error]
|
|
1000
1295
|
}
|
|
1001
1296
|
}
|
|
1002
1297
|
]);
|
|
1298
|
+
// resend the message with device_fanout=false, use at your own risk
|
|
1299
|
+
// if (attrs.error === '475') {
|
|
1300
|
+
// const msg = await getMessage(key)
|
|
1301
|
+
// if (msg) {
|
|
1302
|
+
// await relayMessage(key.remoteJid!, msg, {
|
|
1303
|
+
// messageId: key.id!,
|
|
1304
|
+
// useUserDevicesCache: false,
|
|
1305
|
+
// additionalAttributes: {
|
|
1306
|
+
// device_fanout: 'false'
|
|
1307
|
+
// }
|
|
1308
|
+
// })
|
|
1309
|
+
// }
|
|
1310
|
+
// }
|
|
1003
1311
|
}
|
|
1004
1312
|
};
|
|
1005
1313
|
/// processes a node with the given function
|
|
1006
1314
|
/// and adds the task to the existing buffer if we're buffering events
|
|
1007
|
-
/// Uses createBufferedFunction to be nested-buffer-safe: won't underflow
|
|
1008
|
-
/// the buffersInProgress counter that the offline phase is holding open
|
|
1009
1315
|
const processNodeWithBuffer = async (node, identifier, exec) => {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
);
|
|
1013
|
-
|
|
1316
|
+
ev.buffer();
|
|
1317
|
+
await execTask();
|
|
1318
|
+
ev.flush();
|
|
1319
|
+
function execTask() {
|
|
1320
|
+
return exec(node, false).catch(err => onUnexpectedError(err, identifier));
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
/** Yields control to the event loop to prevent blocking */
|
|
1324
|
+
const yieldToEventLoop = () => {
|
|
1325
|
+
return new Promise(resolve => setImmediate(resolve));
|
|
1014
1326
|
};
|
|
1015
1327
|
const makeOfflineNodeProcessor = () => {
|
|
1016
1328
|
const nodeProcessorMap = new Map([
|
|
@@ -1021,6 +1333,8 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1021
1333
|
]);
|
|
1022
1334
|
const nodes = [];
|
|
1023
1335
|
let isProcessing = false;
|
|
1336
|
+
// Number of nodes to process before yielding to event loop
|
|
1337
|
+
const BATCH_SIZE = 10;
|
|
1024
1338
|
const enqueue = (type, node) => {
|
|
1025
1339
|
nodes.push({ type, node });
|
|
1026
1340
|
if (isProcessing) {
|
|
@@ -1028,11 +1342,8 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1028
1342
|
}
|
|
1029
1343
|
isProcessing = true;
|
|
1030
1344
|
const promise = async () => {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
// Individual handlers (handleMessage, handleReceipt, etc.) already
|
|
1034
|
-
// guard against sending ACKs on a closed connection.
|
|
1035
|
-
while (nodes.length) {
|
|
1345
|
+
let processedInBatch = 0;
|
|
1346
|
+
while (nodes.length && ws.isOpen) {
|
|
1036
1347
|
const { type, node } = nodes.shift();
|
|
1037
1348
|
const nodeProcessor = nodeProcessorMap.get(type);
|
|
1038
1349
|
if (!nodeProcessor) {
|
|
@@ -1040,6 +1351,13 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1040
1351
|
continue;
|
|
1041
1352
|
}
|
|
1042
1353
|
await nodeProcessor(node);
|
|
1354
|
+
processedInBatch++;
|
|
1355
|
+
// Yield to event loop after processing a batch
|
|
1356
|
+
// This prevents blocking the event loop for too long when there are many offline nodes
|
|
1357
|
+
if (processedInBatch >= BATCH_SIZE) {
|
|
1358
|
+
processedInBatch = 0;
|
|
1359
|
+
await yieldToEventLoop();
|
|
1360
|
+
}
|
|
1043
1361
|
}
|
|
1044
1362
|
isProcessing = false;
|
|
1045
1363
|
};
|
|
@@ -1048,33 +1366,35 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1048
1366
|
return { enqueue };
|
|
1049
1367
|
};
|
|
1050
1368
|
const offlineNodeProcessor = makeOfflineNodeProcessor();
|
|
1051
|
-
const processNode = (type, node, identifier, exec) => {
|
|
1369
|
+
const processNode = async (type, node, identifier, exec) => {
|
|
1052
1370
|
const isOffline = !!node.attrs.offline;
|
|
1053
1371
|
if (isOffline) {
|
|
1054
1372
|
offlineNodeProcessor.enqueue(type, node);
|
|
1055
1373
|
}
|
|
1056
1374
|
else {
|
|
1057
|
-
processNodeWithBuffer(node, identifier, exec);
|
|
1375
|
+
await processNodeWithBuffer(node, identifier, exec);
|
|
1058
1376
|
}
|
|
1059
1377
|
};
|
|
1060
1378
|
// recv a message
|
|
1061
|
-
ws.on('CB:message', (node) => {
|
|
1062
|
-
processNode('message', node, 'processing message', handleMessage);
|
|
1379
|
+
ws.on('CB:message', async (node) => {
|
|
1380
|
+
await processNode('message', node, 'processing message', handleMessage);
|
|
1063
1381
|
});
|
|
1064
1382
|
ws.on('CB:call', async (node) => {
|
|
1065
|
-
processNode('call', node, 'handling call', handleCall);
|
|
1383
|
+
await processNode('call', node, 'handling call', handleCall);
|
|
1066
1384
|
});
|
|
1067
|
-
ws.on('CB:receipt', node => {
|
|
1068
|
-
processNode('receipt', node, 'handling receipt', handleReceipt);
|
|
1385
|
+
ws.on('CB:receipt', async (node) => {
|
|
1386
|
+
await processNode('receipt', node, 'handling receipt', handleReceipt);
|
|
1069
1387
|
});
|
|
1070
1388
|
ws.on('CB:notification', async (node) => {
|
|
1071
|
-
processNode('notification', node, 'handling notification', handleNotification);
|
|
1389
|
+
await processNode('notification', node, 'handling notification', handleNotification);
|
|
1072
1390
|
});
|
|
1073
1391
|
ws.on('CB:ack,class:message', (node) => {
|
|
1074
|
-
handleBadAck(node)
|
|
1075
|
-
.catch(error => onUnexpectedError(error, 'handling bad ack'));
|
|
1392
|
+
handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'));
|
|
1076
1393
|
});
|
|
1077
|
-
ev.on('call', ([call]) => {
|
|
1394
|
+
ev.on('call', async ([call]) => {
|
|
1395
|
+
if (!call) {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1078
1398
|
// missed call + group call notification message generation
|
|
1079
1399
|
if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
|
|
1080
1400
|
const msg = {
|
|
@@ -1083,21 +1403,23 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1083
1403
|
id: call.id,
|
|
1084
1404
|
fromMe: false
|
|
1085
1405
|
},
|
|
1086
|
-
messageTimestamp:
|
|
1406
|
+
messageTimestamp: unixTimestampSeconds(call.date)
|
|
1087
1407
|
};
|
|
1088
1408
|
if (call.status === 'timeout') {
|
|
1089
1409
|
if (call.isGroup) {
|
|
1090
|
-
msg.messageStubType = call.isVideo
|
|
1410
|
+
msg.messageStubType = call.isVideo
|
|
1411
|
+
? WAMessageStubType.CALL_MISSED_GROUP_VIDEO
|
|
1412
|
+
: WAMessageStubType.CALL_MISSED_GROUP_VOICE;
|
|
1091
1413
|
}
|
|
1092
1414
|
else {
|
|
1093
|
-
msg.messageStubType = call.isVideo ?
|
|
1415
|
+
msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE;
|
|
1094
1416
|
}
|
|
1095
1417
|
}
|
|
1096
1418
|
else {
|
|
1097
1419
|
msg.message = { call: { callKey: Buffer.from(call.id) } };
|
|
1098
1420
|
}
|
|
1099
|
-
const protoMsg =
|
|
1100
|
-
upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
|
|
1421
|
+
const protoMsg = proto.WebMessageInfo.fromObject(msg);
|
|
1422
|
+
await upsertMessage(protoMsg, call.offline ? 'append' : 'notify');
|
|
1101
1423
|
}
|
|
1102
1424
|
});
|
|
1103
1425
|
ev.on('connection.update', ({ isOnline }) => {
|
|
@@ -1111,9 +1433,10 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1111
1433
|
sendMessageAck,
|
|
1112
1434
|
sendRetryRequest,
|
|
1113
1435
|
rejectCall,
|
|
1114
|
-
|
|
1436
|
+
initiateCall,
|
|
1437
|
+
cancelCall,
|
|
1115
1438
|
fetchMessageHistory,
|
|
1116
1439
|
requestPlaceholderResend,
|
|
1440
|
+
messageRetryManager
|
|
1117
1441
|
};
|
|
1118
|
-
};
|
|
1119
|
-
exports.makeMessagesRecvSocket = makeMessagesRecvSocket;
|
|
1442
|
+
};
|