zapo-js 1.1.2 → 1.2.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.
- package/dist/client/WaClient.js +1 -7
- package/dist/client/WaClientFactory.js +7 -4
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +16 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +43 -6
- package/dist/client/coordinators/WaPresenceCoordinator.d.ts +6 -0
- package/dist/client/coordinators/WaPresenceCoordinator.js +8 -2
- package/dist/client/coordinators/WaProfileCoordinator.d.ts +7 -0
- package/dist/client/coordinators/WaProfileCoordinator.js +10 -4
- package/dist/client/coordinators/WaRetryCoordinator.js +1 -1
- package/dist/client/coordinators/WaTrustedContactTokenCoordinator.d.ts +9 -0
- package/dist/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
- package/dist/client/persistence/mailbox.d.ts +6 -0
- package/dist/client/persistence/mailbox.js +2 -2
- package/dist/client/types.d.ts +49 -0
- package/dist/esm/client/WaClient.js +1 -7
- package/dist/esm/client/WaClientFactory.js +8 -5
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +44 -7
- package/dist/esm/client/coordinators/WaPresenceCoordinator.js +8 -2
- package/dist/esm/client/coordinators/WaProfileCoordinator.js +10 -4
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +1 -1
- package/dist/esm/client/coordinators/WaTrustedContactTokenCoordinator.js +23 -7
- package/dist/esm/client/persistence/mailbox.js +2 -2
- package/dist/esm/message/primitives/incoming.js +99 -42
- package/dist/esm/protocol/jid.js +42 -0
- package/dist/esm/retry/reason.js +2 -2
- package/dist/esm/transport/node/WaNodeOrchestrator.js +6 -2
- package/dist/esm/transport/node/builders/message.js +3 -0
- package/dist/esm/transport/node/builders/presence.js +2 -1
- package/dist/esm/transport/node/builders/profile.js +3 -2
- package/dist/esm/transport/node/builders/retry.js +2 -4
- package/dist/index.d.ts +1 -1
- package/dist/message/primitives/incoming.d.ts +3 -1
- package/dist/message/primitives/incoming.js +98 -41
- package/dist/protocol/jid.d.ts +12 -0
- package/dist/protocol/jid.js +44 -0
- package/dist/retry/reason.js +2 -2
- package/dist/transport/node/WaNodeOrchestrator.js +6 -2
- package/dist/transport/node/builders/message.d.ts +1 -0
- package/dist/transport/node/builders/message.js +3 -0
- package/dist/transport/node/builders/presence.d.ts +6 -0
- package/dist/transport/node/builders/presence.js +2 -1
- package/dist/transport/node/builders/profile.d.ts +1 -1
- package/dist/transport/node/builders/profile.js +3 -2
- package/dist/transport/node/builders/retry.js +2 -4
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@ import { writeRandomPadMax16 } from '../../message/encode/padding.js';
|
|
|
12
12
|
import { buildBotInvokeProtoCopy, extractInvokedBotJid, genBotMsgSecret } from '../../message/kinds/bot.js';
|
|
13
13
|
import { proto } from '../../proto.js';
|
|
14
14
|
import { WA_DEFAULTS, WA_NACK_REASONS } from '../../protocol/constants.js';
|
|
15
|
-
import { isBotJid, isGroupJid, isHostedDeviceJid, isLidJid, isNewsletterJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parseSignalAddressFromJid, signalAddressKey, toUserJid } from '../../protocol/jid.js';
|
|
15
|
+
import { canonicalizeOwnAccountJid, isBotJid, isGroupJid, isHostedDeviceJid, isLidJid, isNewsletterJid, isUserJid, normalizeDeviceJid, normalizeRecipientJid, parseJidFull, parseSignalAddressFromJid, signalAddressKey, toUserJid } from '../../protocol/jid.js';
|
|
16
16
|
import { encodeBinaryNode } from '../../transport/binary/index.js';
|
|
17
17
|
import { buildButtonAddonNode, buildDirectMessageFanoutNode, buildGroupSenderKeyMessageNode, buildMetaNode } from '../../transport/node/builders/message.js';
|
|
18
18
|
import { bytesToHex, TEXT_ENCODER } from '../../util/bytes.js';
|
|
@@ -65,17 +65,20 @@ export class WaMessageDispatchCoordinator {
|
|
|
65
65
|
}
|
|
66
66
|
async publishSignalMessage(input, options = {}) {
|
|
67
67
|
this.requireCurrentMeJid('publishSignalMessage');
|
|
68
|
-
const
|
|
68
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
69
|
+
const sessionJid = canonicalizeOwnAccountJid(input.to, credentials?.meJid, credentials?.meLid);
|
|
70
|
+
const address = parseSignalAddressFromJid(sessionJid);
|
|
69
71
|
if (address.server === WA_DEFAULTS.GROUP_SERVER) {
|
|
70
72
|
throw new Error('publishSignalMessage currently supports only direct chats; use sender-key flow for groups');
|
|
71
73
|
}
|
|
72
74
|
this.deps.logger.trace('wa client publish signal message', {
|
|
73
75
|
to: input.to,
|
|
76
|
+
sessionJid,
|
|
74
77
|
type: input.type
|
|
75
78
|
});
|
|
76
79
|
const [paddedPlaintext] = await Promise.all([
|
|
77
80
|
writeRandomPadMax16(input.plaintext),
|
|
78
|
-
this.deps.sessionResolver.ensureSession(address,
|
|
81
|
+
this.deps.sessionResolver.ensureSession(address, sessionJid, input.expectedIdentity)
|
|
79
82
|
]);
|
|
80
83
|
const encrypted = await this.deps.signalProtocol.encryptMessage(address, paddedPlaintext, input.expectedIdentity);
|
|
81
84
|
const messageType = input.type ?? 'text';
|
|
@@ -211,7 +214,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
211
214
|
if (rawSecret &&
|
|
212
215
|
rawSecret.length > 0 &&
|
|
213
216
|
sendOptions.id &&
|
|
214
|
-
needsSecretPersistence(messageWithSecret)) {
|
|
217
|
+
(this.deps.persistAllMessageSecrets || needsSecretPersistence(messageWithSecret))) {
|
|
215
218
|
const meJid = this.deps.getCurrentCredentials()?.meJid ?? '';
|
|
216
219
|
void this.deps.messageSecretStore
|
|
217
220
|
.set(sendOptions.id, { secret: rawSecret, senderJid: meJid })
|
|
@@ -272,11 +275,14 @@ export class WaMessageDispatchCoordinator {
|
|
|
272
275
|
const directRecipientJid = isGroup
|
|
273
276
|
? recipientJid
|
|
274
277
|
: await this.resolveDirectRecipientLid(toUserJid(recipientJid));
|
|
278
|
+
const peerRecipientPn = isGroup
|
|
279
|
+
? undefined
|
|
280
|
+
: await this.resolvePeerRecipientPn(toUserJid(recipientJid), directRecipientJid);
|
|
275
281
|
const publishResult = isGroup
|
|
276
282
|
? this.shouldUseGroupDirectPath(messageWithIcdc)
|
|
277
283
|
? await this.publishGroupDirectMessage(recipientJid, envelope)
|
|
278
284
|
: await this.publishGroupSenderKeyMessage(recipientJid, envelope)
|
|
279
|
-
: await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope);
|
|
285
|
+
: await this.publishDirectSignalMessageWithFanout(directRecipientJid, envelope, peerRecipientPn);
|
|
280
286
|
return upload ? { ...publishResult, upload } : publishResult;
|
|
281
287
|
}
|
|
282
288
|
/**
|
|
@@ -311,6 +317,35 @@ export class WaMessageDispatchCoordinator {
|
|
|
311
317
|
}
|
|
312
318
|
return pnUserJid;
|
|
313
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Resolves the `peer_recipient_pn` cross-reference for a 1:1 send, or
|
|
322
|
+
* `undefined` to drop the attribute. Only set when the envelope is
|
|
323
|
+
* LID-addressed (`directRecipientJid` is a LID): the PN is the caller's own
|
|
324
|
+
* JID when they passed a PN (zapo switched it to LID for sending), else the
|
|
325
|
+
* device-list snapshot counterpart for a recipient passed in LID form.
|
|
326
|
+
* Mirrors wa-web, which sets `peer_recipient_pn = getPhoneNumber($)` for a
|
|
327
|
+
* LID destination.
|
|
328
|
+
*/
|
|
329
|
+
async resolvePeerRecipientPn(recipientUserJid, directRecipientJid) {
|
|
330
|
+
if (!isLidJid(directRecipientJid))
|
|
331
|
+
return undefined;
|
|
332
|
+
if (isUserJid(recipientUserJid))
|
|
333
|
+
return recipientUserJid;
|
|
334
|
+
try {
|
|
335
|
+
const snapshot = await this.deps.deviceListStore.findByAnyUserJid(directRecipientJid);
|
|
336
|
+
if (snapshot?.userJid && isUserJid(snapshot.userJid))
|
|
337
|
+
return snapshot.userJid;
|
|
338
|
+
if (snapshot?.altUserJid && isUserJid(snapshot.altUserJid))
|
|
339
|
+
return snapshot.altUserJid;
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
this.deps.logger.trace('peer_recipient_pn store lookup failed', {
|
|
343
|
+
lid: directRecipientJid,
|
|
344
|
+
message: toError(error).message
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
314
349
|
async syncSignalSession(jid, reasonIdentity = false) {
|
|
315
350
|
const address = parseSignalAddressFromJid(jid);
|
|
316
351
|
if (address.server === WA_DEFAULTS.GROUP_SERVER) {
|
|
@@ -322,7 +357,8 @@ export class WaMessageDispatchCoordinator {
|
|
|
322
357
|
await this.deps.messageClient.sendReceipt(input);
|
|
323
358
|
}
|
|
324
359
|
async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
|
|
325
|
-
const
|
|
360
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
361
|
+
const meJid = credentials?.meJid;
|
|
326
362
|
const meParsed = meJid ? parseJidFull(meJid) : undefined;
|
|
327
363
|
const meUserJid = meParsed?.userJid;
|
|
328
364
|
let senderIcdc = null;
|
|
@@ -990,7 +1026,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
990
1026
|
distributionParticipants
|
|
991
1027
|
};
|
|
992
1028
|
}
|
|
993
|
-
async publishDirectSignalMessageWithFanout(recipientJid, envelope) {
|
|
1029
|
+
async publishDirectSignalMessageWithFanout(recipientJid, envelope, peerRecipientPn) {
|
|
994
1030
|
const { message, plaintext, type, edit, mediatype, sendOptions } = envelope;
|
|
995
1031
|
const meJid = this.requireCurrentMeJid('sendMessage');
|
|
996
1032
|
const meLid = this.deps.getCurrentCredentials()?.meLid;
|
|
@@ -1153,6 +1189,7 @@ export class WaMessageDispatchCoordinator {
|
|
|
1153
1189
|
customNodes: customNodes.length > 0 ? customNodes : undefined,
|
|
1154
1190
|
mediatype,
|
|
1155
1191
|
decryptFail: envelope.decryptFail,
|
|
1192
|
+
peerRecipientPn,
|
|
1156
1193
|
additionalAttributes: sendOptions.additionalAttributes
|
|
1157
1194
|
});
|
|
1158
1195
|
const replayPayload = {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { isGroupJid } from '../../protocol/jid.js';
|
|
1
2
|
import { buildChatstateNode } from '../../transport/node/builders/chatstate.js';
|
|
2
3
|
import { buildPresenceNode, buildPresenceSubscribeNode } from '../../transport/node/builders/presence.js';
|
|
3
4
|
/** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
|
|
4
5
|
export function createPresenceCoordinator(options) {
|
|
5
|
-
const { sendNode, getCurrentCredentials } = options;
|
|
6
|
+
const { sendNode, getCurrentCredentials, resolvePrivacyTokenNode } = options;
|
|
6
7
|
return {
|
|
7
8
|
send: async (type) => {
|
|
8
9
|
const credentials = getCurrentCredentials();
|
|
@@ -12,7 +13,12 @@ export function createPresenceCoordinator(options) {
|
|
|
12
13
|
await sendNode(buildChatstateNode({ jid, ...opts }));
|
|
13
14
|
},
|
|
14
15
|
subscribe: async (jid, opts) => {
|
|
15
|
-
|
|
16
|
+
const privacyTokenNode = isGroupJid(jid) ? null : await resolvePrivacyTokenNode(jid);
|
|
17
|
+
await sendNode(buildPresenceSubscribeNode({
|
|
18
|
+
jid,
|
|
19
|
+
...opts,
|
|
20
|
+
...(privacyTokenNode ? { privacyTokenNode } : {})
|
|
21
|
+
}));
|
|
16
22
|
}
|
|
17
23
|
};
|
|
18
24
|
}
|
|
@@ -192,10 +192,11 @@ function buildTextStatusMutationInput(input) {
|
|
|
192
192
|
}
|
|
193
193
|
/** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
|
|
194
194
|
export function createProfileCoordinator(options) {
|
|
195
|
-
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, logger } = options;
|
|
195
|
+
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, resolvePrivacyTokenNode, logger } = options;
|
|
196
196
|
return {
|
|
197
197
|
getProfilePicture: async (jid, type, existingId) => {
|
|
198
|
-
const
|
|
198
|
+
const privacyTokenNode = (await resolvePrivacyTokenNode(jid)) ?? undefined;
|
|
199
|
+
const node = buildGetProfilePictureIq(jid, type, existingId, privacyTokenNode);
|
|
199
200
|
const result = await queryWithContext('profile.getPicture', node, undefined, {
|
|
200
201
|
jid,
|
|
201
202
|
type: type ?? 'preview'
|
|
@@ -222,10 +223,11 @@ export function createProfileCoordinator(options) {
|
|
|
222
223
|
getStatus: async (jid) => {
|
|
223
224
|
const sid = await generateSid();
|
|
224
225
|
const queryNodes = buildGetStatusUsyncQueryNodes();
|
|
226
|
+
const privacyTokenNode = await resolvePrivacyTokenNode(jid);
|
|
225
227
|
const usyncNode = buildUsyncIq({
|
|
226
228
|
sid,
|
|
227
229
|
queryProtocolNodes: [queryNodes[1]],
|
|
228
|
-
users: [{ jid }]
|
|
230
|
+
users: [{ jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) }]
|
|
229
231
|
});
|
|
230
232
|
const result = await queryWithContext('profile.getStatus', usyncNode, undefined, {
|
|
231
233
|
jid
|
|
@@ -253,10 +255,14 @@ export function createProfileCoordinator(options) {
|
|
|
253
255
|
}
|
|
254
256
|
const sid = await generateSid();
|
|
255
257
|
const queryProtocolNodes = buildGetStatusUsyncQueryNodes();
|
|
258
|
+
const users = await Promise.all(jids.map(async (jid) => {
|
|
259
|
+
const privacyTokenNode = await resolvePrivacyTokenNode(jid);
|
|
260
|
+
return { jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) };
|
|
261
|
+
}));
|
|
256
262
|
const usyncNode = buildUsyncIq({
|
|
257
263
|
sid,
|
|
258
264
|
queryProtocolNodes,
|
|
259
|
-
users
|
|
265
|
+
users
|
|
260
266
|
});
|
|
261
267
|
const result = await queryWithContext('profile.getProfiles', usyncNode, undefined, {
|
|
262
268
|
count: jids.length
|
|
@@ -499,7 +499,7 @@ export class WaRetryCoordinator {
|
|
|
499
499
|
id: request.keyBundle.key.id,
|
|
500
500
|
publicKey: request.keyBundle.key.publicKey
|
|
501
501
|
}
|
|
502
|
-
});
|
|
502
|
+
}, { reuseExisting: true });
|
|
503
503
|
return this.applySessionBaseKeyPolicy(request, requesterJid, requesterAddress, requesterNormalizedDeviceJid);
|
|
504
504
|
}
|
|
505
505
|
const sessionStillExists = currentSession !== null && !regIdMismatch;
|
|
@@ -39,14 +39,30 @@ export class WaTrustedContactTokenCoordinator {
|
|
|
39
39
|
maxDurationS
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Resolves the receiver-mode `<tctoken>` node to echo back on outbound
|
|
44
|
+
* queries against a contact's privacy-gated data (presence subscribe,
|
|
45
|
+
* profile-picture get, about/status usync). Returns the token only when a
|
|
46
|
+
* non-expired receiver token exists for `jid`; unlike
|
|
47
|
+
* {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
|
|
48
|
+
* (the gated query flows attach the trusted-contact token only).
|
|
49
|
+
*/
|
|
50
|
+
async resolveReceiverTokenNode(jid) {
|
|
51
|
+
const record = await this.store.getByJid(jid);
|
|
52
|
+
if (!record?.tcToken || record.tcTokenTimestamp === undefined) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
45
55
|
const config = this.resolveConfig();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
const nowS = this.serverClock.nowSeconds();
|
|
57
|
+
if (isTokenExpired(record.tcTokenTimestamp, nowS, config.durationS, config.numBuckets)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return buildTcTokenMessageNode(record.tcToken);
|
|
61
|
+
}
|
|
62
|
+
async resolveTokenForMessage(recipientJid) {
|
|
63
|
+
const tcTokenNode = await this.resolveReceiverTokenNode(recipientJid);
|
|
64
|
+
if (tcTokenNode) {
|
|
65
|
+
return tcTokenNode;
|
|
50
66
|
}
|
|
51
67
|
const nctSalt = await this.getNctSalt();
|
|
52
68
|
if (!nctSalt) {
|
|
@@ -53,7 +53,7 @@ function persistContacts(writeBehind, event, nowMs) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
export function persistIncomingMailboxEntities(options) {
|
|
56
|
-
const { logger, writeBehind, messageSecretStore, event } = options;
|
|
56
|
+
const { logger, writeBehind, messageSecretStore, persistAllSecrets, event } = options;
|
|
57
57
|
const stanzaId = event.key.id;
|
|
58
58
|
const chatJid = event.key.remoteJid;
|
|
59
59
|
if (!stanzaId || !chatJid) {
|
|
@@ -78,7 +78,7 @@ export function persistIncomingMailboxEntities(options) {
|
|
|
78
78
|
if (rawSecret &&
|
|
79
79
|
rawSecret.length > 0 &&
|
|
80
80
|
event.message &&
|
|
81
|
-
needsSecretPersistence(event.message)) {
|
|
81
|
+
(persistAllSecrets || needsSecretPersistence(event.message))) {
|
|
82
82
|
const rawSender = event.key.participant ?? event.rawNode.attrs.participant ?? event.key.remoteJid;
|
|
83
83
|
const senderJid = rawSender ? toUserJid(rawSender) : '';
|
|
84
84
|
void messageSecretStore
|
|
@@ -4,7 +4,7 @@ import { unpadPkcs7 } from '../encode/padding.js';
|
|
|
4
4
|
import { processIncomingNewsletterMessage } from '../kinds/newsletter.js';
|
|
5
5
|
import { proto } from '../../proto.js';
|
|
6
6
|
import { WA_MESSAGE_TAGS, WA_MESSAGE_TYPES } from '../../protocol/constants.js';
|
|
7
|
-
import { isBroadcastJid, isGroupJid, isNewsletterJid, parseJidFull, parseSignalAddressFromJid, toUserJid } from '../../protocol/jid.js';
|
|
7
|
+
import { canonicalizeOwnAccountJid, isBroadcastJid, isGroupJid, isNewsletterJid, isOwnAccountJid, parseJidFull, parseSignalAddressFromJid, toUserJid } from '../../protocol/jid.js';
|
|
8
8
|
import { buildAckNode, buildReceiptNode } from '../../transport/node/builders/global.js';
|
|
9
9
|
import { decodeNodeContentBase64OrBytes, findNodeChild } from '../../transport/node/helpers.js';
|
|
10
10
|
import { longToNumber, parseOptionalInt, toError } from '../../util/primitives.js';
|
|
@@ -27,6 +27,52 @@ function extractMessageIdentityAttrs(attrs) {
|
|
|
27
27
|
pushName: attrs.notify
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Self-authored 1:1 chat is the recipient, so its alternate addressing is the
|
|
32
|
+
* `recipient*` attrs (the `sender*` attrs describe me). Promotes `recipientAlt`
|
|
33
|
+
* to `remoteJidAlt` and drops the stale sender/recipient fields.
|
|
34
|
+
*/
|
|
35
|
+
function promoteRecipientAddressing(identity) {
|
|
36
|
+
return {
|
|
37
|
+
...(identity.recipientAlt ? { remoteJidAlt: identity.recipientAlt } : {}),
|
|
38
|
+
...(identity.senderUsername !== undefined
|
|
39
|
+
? { senderUsername: identity.senderUsername }
|
|
40
|
+
: {})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the {@link WaIncomingMessageKey} for a decrypted stanza. `remoteJid`
|
|
45
|
+
* is the chat: the `from`, or the recipient (`recipient` attr, then the
|
|
46
|
+
* deviceSentMessage `destinationJid`) when the message is `fromMe`.
|
|
47
|
+
*/
|
|
48
|
+
function buildIncomingMessageKey(node, sender, options, destinationJid) {
|
|
49
|
+
const fromUserJid = node.attrs.from ? toUserJid(node.attrs.from) : node.attrs.from;
|
|
50
|
+
const isGroup = fromUserJid ? isGroupJid(fromUserJid) : false;
|
|
51
|
+
const isBroadcast = fromUserJid ? isBroadcastJid(fromUserJid) : false;
|
|
52
|
+
const fromMe = sender
|
|
53
|
+
? isOwnAccountJid(sender.userJid, options.getMeJid?.(), options.getMeLid?.())
|
|
54
|
+
: false;
|
|
55
|
+
const selfSentChat = fromMe && !isGroup && !isBroadcast
|
|
56
|
+
? (node.attrs.recipient ?? destinationJid ?? undefined)
|
|
57
|
+
: undefined;
|
|
58
|
+
const chatJid = selfSentChat ? toUserJid(selfSentChat) : fromUserJid;
|
|
59
|
+
const { pushName, ...identity } = extractMessageIdentityAttrs(node.attrs);
|
|
60
|
+
const keyIdentity = selfSentChat ? promoteRecipientAddressing(identity) : identity;
|
|
61
|
+
return {
|
|
62
|
+
pushName,
|
|
63
|
+
key: {
|
|
64
|
+
remoteJid: chatJid ?? '',
|
|
65
|
+
id: node.attrs.id ?? '',
|
|
66
|
+
fromMe,
|
|
67
|
+
isGroup,
|
|
68
|
+
isBroadcast,
|
|
69
|
+
isNewsletter: false,
|
|
70
|
+
...keyIdentity,
|
|
71
|
+
senderDevice: sender?.device ?? 0,
|
|
72
|
+
...((isGroup || isBroadcast) && sender ? { participant: sender.userJid } : {})
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
30
76
|
function pickNextRetryCount(node) {
|
|
31
77
|
const retryNode = findNodeChild(node, 'retry');
|
|
32
78
|
const parsed = parseOptionalInt(retryNode?.attrs.count);
|
|
@@ -240,26 +286,11 @@ function processMsmsgEncNode(node, encNode, senderJid, options) {
|
|
|
240
286
|
encPayload: decoded.encPayload
|
|
241
287
|
}
|
|
242
288
|
};
|
|
243
|
-
const chatJid = node.attrs.from ? toUserJid(node.attrs.from) : node.attrs.from;
|
|
244
289
|
const sender = senderJid ? parseJidFull(senderJid) : null;
|
|
245
|
-
const
|
|
246
|
-
const isBroadcast = chatJid ? isBroadcastJid(chatJid) : false;
|
|
247
|
-
const { pushName, ...keyIdentity } = extractMessageIdentityAttrs(node.attrs);
|
|
290
|
+
const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
|
|
248
291
|
options.emitIncomingMessage?.({
|
|
249
292
|
rawNode: buildIncomingEventRawNode(node),
|
|
250
|
-
key
|
|
251
|
-
remoteJid: chatJid ?? '',
|
|
252
|
-
id: node.attrs.id ?? '',
|
|
253
|
-
fromMe: false,
|
|
254
|
-
isGroup,
|
|
255
|
-
isBroadcast,
|
|
256
|
-
isNewsletter: false,
|
|
257
|
-
...keyIdentity,
|
|
258
|
-
...((isGroup || isBroadcast) && sender?.userJid
|
|
259
|
-
? { participant: sender.userJid }
|
|
260
|
-
: {}),
|
|
261
|
-
senderDevice: sender?.address.device ?? 0
|
|
262
|
-
},
|
|
293
|
+
key,
|
|
263
294
|
stanzaType: node.attrs.type,
|
|
264
295
|
offline: node.attrs.offline !== undefined,
|
|
265
296
|
timestampSeconds: parseOptionalInt(node.attrs.t),
|
|
@@ -313,29 +344,14 @@ async function decryptAndProcessEncNode(node, encNode, encType, senderJid, optio
|
|
|
313
344
|
}
|
|
314
345
|
}
|
|
315
346
|
if (shouldEmitIncomingMessage(message)) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const chatJid = fromAttr ? toUserJid(fromAttr) : fromAttr;
|
|
321
|
-
const isGroup = chatJid ? isGroupJid(chatJid) : false;
|
|
322
|
-
const isBroadcast = chatJid ? isBroadcastJid(chatJid) : false;
|
|
323
|
-
const senderUserJid = `${senderAddress.user}@${senderAddress.server}`;
|
|
324
|
-
const { pushName, ...keyIdentity } = extractMessageIdentityAttrs(node.attrs);
|
|
347
|
+
const { key, pushName } = buildIncomingMessageKey(node, {
|
|
348
|
+
userJid: `${senderAddress.user}@${senderAddress.server}`,
|
|
349
|
+
device: senderAddress.device
|
|
350
|
+
}, options, decodedMessage.deviceSentMessage?.destinationJid ?? undefined);
|
|
325
351
|
const expirationSeconds = pickIncomingExpirationSeconds(message);
|
|
326
352
|
options.emitIncomingMessage?.({
|
|
327
353
|
rawNode: buildIncomingEventRawNode(node),
|
|
328
|
-
key
|
|
329
|
-
remoteJid: chatJid ?? '',
|
|
330
|
-
id: node.attrs.id ?? '',
|
|
331
|
-
fromMe: false,
|
|
332
|
-
isGroup,
|
|
333
|
-
isBroadcast,
|
|
334
|
-
isNewsletter: false,
|
|
335
|
-
...keyIdentity,
|
|
336
|
-
senderDevice: senderAddress.device,
|
|
337
|
-
...(isGroup || isBroadcast ? { participant: senderUserJid } : {})
|
|
338
|
-
},
|
|
354
|
+
key,
|
|
339
355
|
stanzaType: node.attrs.type,
|
|
340
356
|
offline: node.attrs.offline !== undefined,
|
|
341
357
|
timestampSeconds: parseOptionalInt(node.attrs.t),
|
|
@@ -414,10 +430,15 @@ export async function handleIncomingMessageAck(node, options) {
|
|
|
414
430
|
continue;
|
|
415
431
|
}
|
|
416
432
|
const encType = child.attrs.type === 'msg' ? 'msg' : 'pkmsg';
|
|
417
|
-
result = await decryptAndProcessEncNode(node, child, encType, senderJid, options, (ciphertext, senderAddress) =>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
433
|
+
result = await decryptAndProcessEncNode(node, child, encType, senderJid, options, (ciphertext, senderAddress) => {
|
|
434
|
+
const sessionJid = canonicalizeOwnAccountJid(senderJid, options.getMeJid?.(), options.getMeLid?.());
|
|
435
|
+
return options.signalProtocol.decryptMessage(sessionJid === senderJid
|
|
436
|
+
? senderAddress
|
|
437
|
+
: parseSignalAddressFromJid(sessionJid), {
|
|
438
|
+
type: encType,
|
|
439
|
+
ciphertext
|
|
440
|
+
});
|
|
441
|
+
});
|
|
421
442
|
break;
|
|
422
443
|
}
|
|
423
444
|
case 'msmsg': {
|
|
@@ -475,6 +496,42 @@ export async function handleIncomingMessageAck(node, options) {
|
|
|
475
496
|
await options.sendNode(ackNode);
|
|
476
497
|
return true;
|
|
477
498
|
}
|
|
499
|
+
const unavailableNode = findNodeChild(node, 'unavailable');
|
|
500
|
+
if (unavailableNode) {
|
|
501
|
+
const kind = unavailableNode.attrs.hosted === 'true'
|
|
502
|
+
? 'hosted'
|
|
503
|
+
: unavailableNode.attrs.type === 'view_once'
|
|
504
|
+
? 'view_once'
|
|
505
|
+
: 'other';
|
|
506
|
+
const senderJid = node.attrs.participant ?? node.attrs.from;
|
|
507
|
+
const sender = senderJid ? parseJidFull(senderJid) : null;
|
|
508
|
+
const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
|
|
509
|
+
options.emitUnavailableMessage?.({
|
|
510
|
+
rawNode: buildIncomingEventRawNode(node),
|
|
511
|
+
key,
|
|
512
|
+
kind,
|
|
513
|
+
stanzaType: node.attrs.type,
|
|
514
|
+
offline: node.attrs.offline !== undefined,
|
|
515
|
+
timestampSeconds: parseOptionalInt(node.attrs.t),
|
|
516
|
+
pushName
|
|
517
|
+
});
|
|
518
|
+
const ackNode = buildAckNode({
|
|
519
|
+
kind: 'message',
|
|
520
|
+
node,
|
|
521
|
+
id,
|
|
522
|
+
to: from,
|
|
523
|
+
from: options.getMeJid?.()
|
|
524
|
+
});
|
|
525
|
+
options.logger.trace('acking unavailable incoming message', {
|
|
526
|
+
id,
|
|
527
|
+
to: from,
|
|
528
|
+
type: ackNode.attrs.type,
|
|
529
|
+
participant: ackNode.attrs.participant,
|
|
530
|
+
unavailableKind: kind
|
|
531
|
+
});
|
|
532
|
+
await options.sendNode(ackNode);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
478
535
|
if (!shouldSendStandardReceipt) {
|
|
479
536
|
return true;
|
|
480
537
|
}
|
package/dist/esm/protocol/jid.js
CHANGED
|
@@ -191,6 +191,48 @@ export function toUserJid(jid, options = {}) {
|
|
|
191
191
|
: address.server;
|
|
192
192
|
return `${address.user}@${server}`;
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* True when `jid` is the account's own user, matching the `meJid` (pn) or
|
|
196
|
+
* `meLid` (lid) identity device-insensitively. Mirrors WhatsApp Web's
|
|
197
|
+
* `isMeAccount`.
|
|
198
|
+
*/
|
|
199
|
+
export function isOwnAccountJid(jid, meJid, meLid) {
|
|
200
|
+
const candidateUser = toUserJid(jid);
|
|
201
|
+
return ((!!meJid && toUserJid(meJid) === candidateUser) ||
|
|
202
|
+
(!!meLid && toUserJid(meLid) === candidateUser));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Rewrites the account's own PN device JID to its LID equivalent so self traffic
|
|
206
|
+
* keys one LID session instead of forking into separate PN and LID ratchets.
|
|
207
|
+
* No-op for other users, already-LID/unparseable JIDs, or unknown identity.
|
|
208
|
+
*/
|
|
209
|
+
export function canonicalizeOwnAccountJid(jid, meJid, meLid) {
|
|
210
|
+
if (!meJid || !meLid)
|
|
211
|
+
return jid;
|
|
212
|
+
let address;
|
|
213
|
+
try {
|
|
214
|
+
address = parseSignalAddressFromJid(jid);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return jid;
|
|
218
|
+
}
|
|
219
|
+
if ((address.server ?? WA_DEFAULTS.HOST_DOMAIN) !== WA_DEFAULTS.HOST_DOMAIN)
|
|
220
|
+
return jid;
|
|
221
|
+
let mePnUser;
|
|
222
|
+
let meLidUser;
|
|
223
|
+
try {
|
|
224
|
+
mePnUser = parseSignalAddressFromJid(meJid).user;
|
|
225
|
+
meLidUser = parseSignalAddressFromJid(meLid).user;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return jid;
|
|
229
|
+
}
|
|
230
|
+
if (address.user !== mePnUser)
|
|
231
|
+
return jid;
|
|
232
|
+
return address.device === 0
|
|
233
|
+
? `${meLidUser}@${WA_DEFAULTS.LID_SERVER}`
|
|
234
|
+
: `${meLidUser}:${address.device}@${WA_DEFAULTS.LID_SERVER}`;
|
|
235
|
+
}
|
|
194
236
|
/**
|
|
195
237
|
* Returns the JID in its full device form. JIDs with `device === 0` lose the
|
|
196
238
|
* device segment (`user@server`); all others keep `user:device@server`.
|
package/dist/esm/retry/reason.js
CHANGED
|
@@ -17,10 +17,10 @@ const RETRY_REASON_MATCHERS = [
|
|
|
17
17
|
},
|
|
18
18
|
{ matches: ['invalid signature'], code: RETRY_REASON.SignalErrorInvalidSignature },
|
|
19
19
|
{
|
|
20
|
-
matches: ['too many messages in future', 'future message'],
|
|
20
|
+
matches: ['too far in future', 'too many messages in future', 'future message'],
|
|
21
21
|
code: RETRY_REASON.SignalErrorFutureMessage
|
|
22
22
|
},
|
|
23
|
-
{ matches: ['invalid mac'], code: RETRY_REASON.SignalErrorBadMac },
|
|
23
|
+
{ matches: ['invalid message mac', 'invalid mac'], code: RETRY_REASON.SignalErrorBadMac },
|
|
24
24
|
{ matches: ['invalid session'], code: RETRY_REASON.SignalErrorInvalidSession },
|
|
25
25
|
{ matches: ['invalid message key'], code: RETRY_REASON.SignalErrorInvalidMsgKey },
|
|
26
26
|
{
|
|
@@ -21,8 +21,12 @@ export class WaNodeOrchestrator {
|
|
|
21
21
|
return this.pendingQueries.size > 0;
|
|
22
22
|
}
|
|
23
23
|
clearPending(reason) {
|
|
24
|
-
this.
|
|
25
|
-
|
|
24
|
+
const count = this.pendingQueries.size;
|
|
25
|
+
if (count === 0) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.logger.debug('clearing pending node queries', {
|
|
29
|
+
count,
|
|
26
30
|
reason: reason.message
|
|
27
31
|
});
|
|
28
32
|
for (const pending of this.pendingQueries.values()) {
|
|
@@ -32,6 +32,9 @@ function buildMessageAttrs(input) {
|
|
|
32
32
|
if (input.addressingMode) {
|
|
33
33
|
attrs.addressing_mode = input.addressingMode;
|
|
34
34
|
}
|
|
35
|
+
if (input.peerRecipientPn) {
|
|
36
|
+
attrs.peer_recipient_pn = input.peerRecipientPn;
|
|
37
|
+
}
|
|
35
38
|
if (input.additionalAttributes) {
|
|
36
39
|
Object.assign(attrs, input.additionalAttributes);
|
|
37
40
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WA_DEFAULTS, WA_IQ_TYPES, WA_NODE_TAGS, WA_XMLNS } from '../../../protocol/constants.js';
|
|
2
2
|
import { buildIqNode } from '../../node/query.js';
|
|
3
|
-
export function buildGetProfilePictureIq(targetJid, type = 'preview', existingId) {
|
|
3
|
+
export function buildGetProfilePictureIq(targetJid, type = 'preview', existingId, privacyTokenNode) {
|
|
4
4
|
const pictureAttrs = {
|
|
5
5
|
type,
|
|
6
6
|
query: 'url'
|
|
@@ -11,7 +11,8 @@ export function buildGetProfilePictureIq(targetJid, type = 'preview', existingId
|
|
|
11
11
|
return buildIqNode(WA_IQ_TYPES.GET, WA_DEFAULTS.HOST_DOMAIN, WA_XMLNS.PROFILE_PICTURE, [
|
|
12
12
|
{
|
|
13
13
|
tag: WA_NODE_TAGS.PICTURE,
|
|
14
|
-
attrs: pictureAttrs
|
|
14
|
+
attrs: pictureAttrs,
|
|
15
|
+
...(privacyTokenNode ? { content: [privacyTokenNode] } : {})
|
|
15
16
|
}
|
|
16
17
|
], {
|
|
17
18
|
target: targetJid
|
|
@@ -73,11 +73,9 @@ export function buildRetryReceiptNode(input) {
|
|
|
73
73
|
v: RETRY_RECEIPT_VERSION,
|
|
74
74
|
count: String(input.retryCount),
|
|
75
75
|
id: input.originalMsgId,
|
|
76
|
-
t: input.t
|
|
76
|
+
t: input.t,
|
|
77
|
+
error: String(input.error ?? 0)
|
|
77
78
|
};
|
|
78
|
-
if (input.error !== undefined && input.error !== 0) {
|
|
79
|
-
retryAttrs.error = String(input.error);
|
|
80
|
-
}
|
|
81
79
|
const content = [
|
|
82
80
|
{
|
|
83
81
|
tag: 'retry',
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { WaClient } from './client';
|
|
2
2
|
export type { WaClientEventMap, WaClientOptions, WaClientProxyOptions, WaDownloadMediaOptions, WaHistorySyncChunkEvent, WaHistorySyncOptions, WaWriteBehindOptions } from './client/types';
|
|
3
3
|
export type { WaMessageCoordinator } from './client/coordinators/WaMessageCoordinator';
|
|
4
|
-
export type { WaAccountTakeoverNoticeEvent, WaAppStateMutationEvent, WaAppStateMutationSource, WaBusinessEvent, WaBusinessEventAction, WaBusinessProfileResult, WaConnectionEvent, WaGroupEvent, WaGroupEventAction, WaGroupEventLinkedGroup, WaGroupEventMembershipRequest, WaGroupEventParticipant, WaGroupEventSubgroupSuggestion, WaIgnoreKey, WaIgnoreKeyContext, WaIgnoreKeyPredicate, WaIgnoreStanzaKind, WaIncomingAddonEvent, WaIncomingBaseEvent, WaIncomingBotChunkEvent, WaIncomingCallEvent, WaIncomingChatstateEvent, WaIncomingErrorStanzaEvent, WaIncomingFailureEvent, WaIncomingMessageEvent, WaIncomingMessageKey, WaIncomingNewsletterEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingNodeHandler, WaIncomingNodeHandlerRegistration, WaIncomingNotificationEvent, WaIncomingPresenceEvent, WaIncomingProtocolMessageEvent, WaIncomingReceiptEvent, WaIncomingStanzaFilter, WaIncomingUnhandledStanzaEvent, WaMexLidChangeEvent, WaMexMessageCappingEvent, WaMexMessageCappingStatus, WaMexNotificationEvent, WaMexNotificationGraphQlError, WaMexNotificationOperationName, WaMexNotificationUnknownEvent, WaMexOwnUsernameSyncEvent, WaMexTextStatusUpdateEvent, WaMexTextStatusUpdateHintEvent, WaMexUsernameDeleteEvent, WaMexUsernameSetEvent, WaMexUsernameUpdateHintEvent, WaOfflineResumeEvent, WaPictureEvent, WaPictureEventAction, WaPrivacyTokenUpdateEvent, WaReceiptStatus, WaRegistrationCodeEvent, WaSendMessageOptions, WaVerifiedNameResult, WaAddonKind, WaNewsletterEventAction, WaNewsletterMessageUpdate, WaNewsletterPollVoteEntry, WaNewsletterReactionEntry } from './client/types';
|
|
4
|
+
export type { WaAccountTakeoverNoticeEvent, WaAppStateMutationEvent, WaAppStateMutationSource, WaBusinessEvent, WaBusinessEventAction, WaBusinessProfileResult, WaConnectionEvent, WaGroupEvent, WaGroupEventAction, WaGroupEventLinkedGroup, WaGroupEventMembershipRequest, WaGroupEventParticipant, WaGroupEventSubgroupSuggestion, WaIgnoreKey, WaIgnoreKeyContext, WaIgnoreKeyPredicate, WaIgnoreStanzaKind, WaIncomingAddonEvent, WaIncomingBaseEvent, WaIncomingBotChunkEvent, WaIncomingCallEvent, WaIncomingChatstateEvent, WaIncomingErrorStanzaEvent, WaIncomingFailureEvent, WaIncomingMessageEvent, WaIncomingMessageKey, WaIncomingNewsletterEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingNodeHandler, WaIncomingNodeHandlerRegistration, WaIncomingNotificationEvent, WaIncomingPresenceEvent, WaIncomingProtocolMessageEvent, WaIncomingReceiptEvent, WaIncomingStanzaFilter, WaIncomingUnavailableMessageEvent, WaIncomingUnhandledStanzaEvent, WaMexLidChangeEvent, WaMexMessageCappingEvent, WaMexMessageCappingStatus, WaMexNotificationEvent, WaMexNotificationGraphQlError, WaMexNotificationOperationName, WaMexNotificationUnknownEvent, WaMexOwnUsernameSyncEvent, WaMexTextStatusUpdateEvent, WaMexTextStatusUpdateHintEvent, WaMexUsernameDeleteEvent, WaMexUsernameSetEvent, WaMexUsernameUpdateHintEvent, WaOfflineResumeEvent, WaPictureEvent, WaPictureEventAction, WaPrivacyTokenUpdateEvent, WaReceiptStatus, WaRegistrationCodeEvent, WaSendMessageOptions, WaUnavailableMessageKind, WaVerifiedNameResult, WaAddonKind, WaNewsletterEventAction, WaNewsletterMessageUpdate, WaNewsletterPollVoteEntry, WaNewsletterReactionEntry } from './client/types';
|
|
5
5
|
export type { WaAppStateMutationCoordinator, WaBroadcastListParticipant, WaSetBroadcastListInput, WaSetStatusPrivacyInput } from './client/coordinators/WaAppStateMutationCoordinator';
|
|
6
6
|
export type { WaBotCoordinator, WaBotInfo, WaBotPosingAsProfessional, WaBotProfileCommand, WaBotProfilePrompt, WaBotProfileResult, WaBotPromptOptions, WaGetBotProfileOptions } from './client/coordinators/WaBotCoordinator';
|
|
7
7
|
export type { WaBroadcastListCoordinator, WaSendBroadcastListMessageInput } from './client/coordinators/WaBroadcastListCoordinator';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WaIncomingMessageEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingUnhandledStanzaEvent } from '../../client/types';
|
|
1
|
+
import type { WaIncomingMessageEvent, WaIncomingNewsletterMessageUpdateEvent, WaIncomingUnavailableMessageEvent, WaIncomingUnhandledStanzaEvent } from '../../client/types';
|
|
2
2
|
import type { Logger } from '../../infra/log/types';
|
|
3
3
|
import { proto } from '../../proto';
|
|
4
4
|
import type { WaRetryDecryptFailureContext } from '../../retry/types';
|
|
@@ -9,11 +9,13 @@ interface WaIncomingMessageAckHandlerOptions {
|
|
|
9
9
|
readonly logger: Logger;
|
|
10
10
|
readonly sendNode: (node: BinaryNode) => Promise<void>;
|
|
11
11
|
readonly getMeJid?: () => string | null | undefined;
|
|
12
|
+
readonly getMeLid?: () => string | null | undefined;
|
|
12
13
|
readonly signalProtocol?: SignalProtocol;
|
|
13
14
|
readonly senderKeyManager?: SenderKeyManager;
|
|
14
15
|
readonly onDecryptFailure?: (context: WaRetryDecryptFailureContext, error: unknown) => Promise<boolean>;
|
|
15
16
|
readonly emitIncomingMessage?: (event: WaIncomingMessageEvent) => void;
|
|
16
17
|
readonly emitNewsletterMessageUpdate?: (event: WaIncomingNewsletterMessageUpdateEvent) => void;
|
|
18
|
+
readonly emitUnavailableMessage?: (event: WaIncomingUnavailableMessageEvent) => void;
|
|
17
19
|
readonly emitUnhandledStanza?: (event: WaIncomingUnhandledStanzaEvent) => void;
|
|
18
20
|
}
|
|
19
21
|
/**
|