zapo-js 1.1.2 → 1.1.3
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/WaClientFactory.js +5 -4
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +7 -3
- 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/esm/client/WaClientFactory.js +6 -5
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +8 -4
- 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/message/primitives/incoming.js +63 -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/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/message/primitives/incoming.d.ts +1 -0
- package/dist/message/primitives/incoming.js +62 -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/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
|
@@ -487,7 +487,8 @@ function buildWaClientDependencies(input) {
|
|
|
487
487
|
});
|
|
488
488
|
const presenceCoordinator = (0, WaPresenceCoordinator_1.createPresenceCoordinator)({
|
|
489
489
|
sendNode: (node) => nodeOrchestrator.sendNode(node, false),
|
|
490
|
-
getCurrentCredentials
|
|
490
|
+
getCurrentCredentials,
|
|
491
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
|
|
491
492
|
});
|
|
492
493
|
const peerDataOperation = (0, peer_data_operation_1.createPeerDataOperationRequester)({
|
|
493
494
|
logger,
|
|
@@ -559,9 +560,7 @@ function buildWaClientDependencies(input) {
|
|
|
559
560
|
const credentials = getCurrentCredentials();
|
|
560
561
|
if (!credentials)
|
|
561
562
|
return false;
|
|
562
|
-
|
|
563
|
-
return ((!!credentials.meJid && (0, jid_1.toUserJid)(credentials.meJid) === candidateUser) ||
|
|
564
|
-
(!!credentials.meLid && (0, jid_1.toUserJid)(credentials.meLid) === candidateUser));
|
|
563
|
+
return (0, jid_1.isOwnAccountJid)(deviceJid, credentials.meJid, credentials.meLid);
|
|
565
564
|
},
|
|
566
565
|
sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
|
|
567
566
|
triggerSync: async () => {
|
|
@@ -604,6 +603,7 @@ function buildWaClientDependencies(input) {
|
|
|
604
603
|
queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
|
|
605
604
|
mutations: appStateMutations,
|
|
606
605
|
applyOwnPushName,
|
|
606
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
|
|
607
607
|
logger
|
|
608
608
|
});
|
|
609
609
|
const statusCoordinator = (0, WaStatusCoordinator_1.createStatusCoordinator)({
|
|
@@ -684,6 +684,7 @@ function buildWaClientDependencies(input) {
|
|
|
684
684
|
logger,
|
|
685
685
|
sendNode: runtime.sendNode,
|
|
686
686
|
getMeJid: () => getCurrentCredentials()?.meJid,
|
|
687
|
+
getMeLid: () => getCurrentCredentials()?.meLid,
|
|
687
688
|
signalProtocol,
|
|
688
689
|
senderKeyManager,
|
|
689
690
|
onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
|
|
@@ -68,17 +68,20 @@ class WaMessageDispatchCoordinator {
|
|
|
68
68
|
}
|
|
69
69
|
async publishSignalMessage(input, options = {}) {
|
|
70
70
|
this.requireCurrentMeJid('publishSignalMessage');
|
|
71
|
-
const
|
|
71
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
72
|
+
const sessionJid = (0, jid_1.canonicalizeOwnAccountJid)(input.to, credentials?.meJid, credentials?.meLid);
|
|
73
|
+
const address = (0, jid_1.parseSignalAddressFromJid)(sessionJid);
|
|
72
74
|
if (address.server === constants_1.WA_DEFAULTS.GROUP_SERVER) {
|
|
73
75
|
throw new Error('publishSignalMessage currently supports only direct chats; use sender-key flow for groups');
|
|
74
76
|
}
|
|
75
77
|
this.deps.logger.trace('wa client publish signal message', {
|
|
76
78
|
to: input.to,
|
|
79
|
+
sessionJid,
|
|
77
80
|
type: input.type
|
|
78
81
|
});
|
|
79
82
|
const [paddedPlaintext] = await Promise.all([
|
|
80
83
|
(0, padding_1.writeRandomPadMax16)(input.plaintext),
|
|
81
|
-
this.deps.sessionResolver.ensureSession(address,
|
|
84
|
+
this.deps.sessionResolver.ensureSession(address, sessionJid, input.expectedIdentity)
|
|
82
85
|
]);
|
|
83
86
|
const encrypted = await this.deps.signalProtocol.encryptMessage(address, paddedPlaintext, input.expectedIdentity);
|
|
84
87
|
const messageType = input.type ?? 'text';
|
|
@@ -325,7 +328,8 @@ class WaMessageDispatchCoordinator {
|
|
|
325
328
|
await this.deps.messageClient.sendReceipt(input);
|
|
326
329
|
}
|
|
327
330
|
async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
|
|
328
|
-
const
|
|
331
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
332
|
+
const meJid = credentials?.meJid;
|
|
329
333
|
const meParsed = meJid ? (0, jid_1.parseJidFull)(meJid) : undefined;
|
|
330
334
|
const meUserJid = meParsed?.userJid;
|
|
331
335
|
let senderIcdc = null;
|
|
@@ -21,6 +21,12 @@ export interface WaPresenceCoordinator {
|
|
|
21
21
|
interface WaPresenceCoordinatorOptions {
|
|
22
22
|
readonly sendNode: (node: BinaryNode) => Promise<void>;
|
|
23
23
|
readonly getCurrentCredentials: () => WaAuthCredentials | null;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the receiver-mode `<tctoken>` node for a contact, echoed back
|
|
26
|
+
* on a user presence subscription to unlock the target's presence
|
|
27
|
+
* visibility. Returns `null` when no valid token is held.
|
|
28
|
+
*/
|
|
29
|
+
readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
|
|
24
30
|
}
|
|
25
31
|
/** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
|
|
26
32
|
export declare function createPresenceCoordinator(options: WaPresenceCoordinatorOptions): WaPresenceCoordinator;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createPresenceCoordinator = createPresenceCoordinator;
|
|
4
|
+
const jid_1 = require("../../protocol/jid");
|
|
4
5
|
const chatstate_1 = require("../../transport/node/builders/chatstate");
|
|
5
6
|
const presence_1 = require("../../transport/node/builders/presence");
|
|
6
7
|
/** Builds a {@link WaPresenceCoordinator} from its node-send dependency. */
|
|
7
8
|
function createPresenceCoordinator(options) {
|
|
8
|
-
const { sendNode, getCurrentCredentials } = options;
|
|
9
|
+
const { sendNode, getCurrentCredentials, resolvePrivacyTokenNode } = options;
|
|
9
10
|
return {
|
|
10
11
|
send: async (type) => {
|
|
11
12
|
const credentials = getCurrentCredentials();
|
|
@@ -15,7 +16,12 @@ function createPresenceCoordinator(options) {
|
|
|
15
16
|
await sendNode((0, chatstate_1.buildChatstateNode)({ jid, ...opts }));
|
|
16
17
|
},
|
|
17
18
|
subscribe: async (jid, opts) => {
|
|
18
|
-
|
|
19
|
+
const privacyTokenNode = (0, jid_1.isGroupJid)(jid) ? null : await resolvePrivacyTokenNode(jid);
|
|
20
|
+
await sendNode((0, presence_1.buildPresenceSubscribeNode)({
|
|
21
|
+
jid,
|
|
22
|
+
...opts,
|
|
23
|
+
...(privacyTokenNode ? { privacyTokenNode } : {})
|
|
24
|
+
}));
|
|
19
25
|
}
|
|
20
26
|
};
|
|
21
27
|
}
|
|
@@ -149,6 +149,13 @@ interface WaProfileCoordinatorOptions {
|
|
|
149
149
|
* idempotent (a no-op when the name already matches).
|
|
150
150
|
*/
|
|
151
151
|
readonly applyOwnPushName: (name: string) => Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Resolves the receiver-mode `<tctoken>` node for a contact, echoed back on
|
|
154
|
+
* privacy-gated profile queries (picture get, about/status usync) to prove
|
|
155
|
+
* this account is a trusted contact. Returns `null` when no valid token is
|
|
156
|
+
* held for the JID.
|
|
157
|
+
*/
|
|
158
|
+
readonly resolvePrivacyTokenNode: (jid: string) => Promise<BinaryNode | null>;
|
|
152
159
|
readonly logger: Logger;
|
|
153
160
|
}
|
|
154
161
|
/** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
|
|
@@ -195,10 +195,11 @@ function buildTextStatusMutationInput(input) {
|
|
|
195
195
|
}
|
|
196
196
|
/** Builds a {@link WaProfileCoordinator} from its IQ/MEX/SID dependencies. */
|
|
197
197
|
function createProfileCoordinator(options) {
|
|
198
|
-
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, logger } = options;
|
|
198
|
+
const { queryWithContext, generateSid, mexSocket, queryLidsByPhoneJids, mutations, applyOwnPushName, resolvePrivacyTokenNode, logger } = options;
|
|
199
199
|
return {
|
|
200
200
|
getProfilePicture: async (jid, type, existingId) => {
|
|
201
|
-
const
|
|
201
|
+
const privacyTokenNode = (await resolvePrivacyTokenNode(jid)) ?? undefined;
|
|
202
|
+
const node = (0, profile_1.buildGetProfilePictureIq)(jid, type, existingId, privacyTokenNode);
|
|
202
203
|
const result = await queryWithContext('profile.getPicture', node, undefined, {
|
|
203
204
|
jid,
|
|
204
205
|
type: type ?? 'preview'
|
|
@@ -225,10 +226,11 @@ function createProfileCoordinator(options) {
|
|
|
225
226
|
getStatus: async (jid) => {
|
|
226
227
|
const sid = await generateSid();
|
|
227
228
|
const queryNodes = (0, profile_1.buildGetStatusUsyncQueryNodes)();
|
|
229
|
+
const privacyTokenNode = await resolvePrivacyTokenNode(jid);
|
|
228
230
|
const usyncNode = (0, usync_1.buildUsyncIq)({
|
|
229
231
|
sid,
|
|
230
232
|
queryProtocolNodes: [queryNodes[1]],
|
|
231
|
-
users: [{ jid }]
|
|
233
|
+
users: [{ jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) }]
|
|
232
234
|
});
|
|
233
235
|
const result = await queryWithContext('profile.getStatus', usyncNode, undefined, {
|
|
234
236
|
jid
|
|
@@ -256,10 +258,14 @@ function createProfileCoordinator(options) {
|
|
|
256
258
|
}
|
|
257
259
|
const sid = await generateSid();
|
|
258
260
|
const queryProtocolNodes = (0, profile_1.buildGetStatusUsyncQueryNodes)();
|
|
261
|
+
const users = await Promise.all(jids.map(async (jid) => {
|
|
262
|
+
const privacyTokenNode = await resolvePrivacyTokenNode(jid);
|
|
263
|
+
return { jid, ...(privacyTokenNode ? { content: [privacyTokenNode] } : {}) };
|
|
264
|
+
}));
|
|
259
265
|
const usyncNode = (0, usync_1.buildUsyncIq)({
|
|
260
266
|
sid,
|
|
261
267
|
queryProtocolNodes,
|
|
262
|
-
users
|
|
268
|
+
users
|
|
263
269
|
});
|
|
264
270
|
const result = await queryWithContext('profile.getProfiles', usyncNode, undefined, {
|
|
265
271
|
count: jids.length
|
|
@@ -502,7 +502,7 @@ class WaRetryCoordinator {
|
|
|
502
502
|
id: request.keyBundle.key.id,
|
|
503
503
|
publicKey: request.keyBundle.key.publicKey
|
|
504
504
|
}
|
|
505
|
-
});
|
|
505
|
+
}, { reuseExisting: true });
|
|
506
506
|
return this.applySessionBaseKeyPolicy(request, requesterJid, requesterAddress, requesterNormalizedDeviceJid);
|
|
507
507
|
}
|
|
508
508
|
const sessionStillExists = currentSession !== null && !regIdMismatch;
|
|
@@ -41,6 +41,15 @@ export declare class WaTrustedContactTokenCoordinator {
|
|
|
41
41
|
readonly getConfigOverrides?: () => Partial<WaTrustedContactTokenConfig>;
|
|
42
42
|
});
|
|
43
43
|
private resolveConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the receiver-mode `<tctoken>` node to echo back on outbound
|
|
46
|
+
* queries against a contact's privacy-gated data (presence subscribe,
|
|
47
|
+
* profile-picture get, about/status usync). Returns the token only when a
|
|
48
|
+
* non-expired receiver token exists for `jid`; unlike
|
|
49
|
+
* {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
|
|
50
|
+
* (the gated query flows attach the trusted-contact token only).
|
|
51
|
+
*/
|
|
52
|
+
resolveReceiverTokenNode(jid: string): Promise<BinaryNode | null>;
|
|
44
53
|
resolveTokenForMessage(recipientJid: string): Promise<BinaryNode | null>;
|
|
45
54
|
handleIncomingToken(fromJid: string, tokens: readonly ParsedPrivacyToken[]): Promise<void>;
|
|
46
55
|
maybeIssueSenderToken(recipientJid: string): Promise<void>;
|
|
@@ -42,14 +42,30 @@ class WaTrustedContactTokenCoordinator {
|
|
|
42
42
|
maxDurationS
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Resolves the receiver-mode `<tctoken>` node to echo back on outbound
|
|
47
|
+
* queries against a contact's privacy-gated data (presence subscribe,
|
|
48
|
+
* profile-picture get, about/status usync). Returns the token only when a
|
|
49
|
+
* non-expired receiver token exists for `jid`; unlike
|
|
50
|
+
* {@link resolveTokenForMessage} it does **not** fall back to a `<cstoken>`
|
|
51
|
+
* (the gated query flows attach the trusted-contact token only).
|
|
52
|
+
*/
|
|
53
|
+
async resolveReceiverTokenNode(jid) {
|
|
54
|
+
const record = await this.store.getByJid(jid);
|
|
55
|
+
if (!record?.tcToken || record.tcTokenTimestamp === undefined) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
48
58
|
const config = this.resolveConfig();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const nowS = this.serverClock.nowSeconds();
|
|
60
|
+
if ((0, tc_token_1.isTokenExpired)(record.tcTokenTimestamp, nowS, config.durationS, config.numBuckets)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return (0, privacy_token_2.buildTcTokenMessageNode)(record.tcToken);
|
|
64
|
+
}
|
|
65
|
+
async resolveTokenForMessage(recipientJid) {
|
|
66
|
+
const tcTokenNode = await this.resolveReceiverTokenNode(recipientJid);
|
|
67
|
+
if (tcTokenNode) {
|
|
68
|
+
return tcTokenNode;
|
|
53
69
|
}
|
|
54
70
|
const nctSalt = await this.getNctSalt();
|
|
55
71
|
if (!nctSalt) {
|
|
@@ -39,7 +39,7 @@ import { handleIncomingMessageAck } from '../message/primitives/incoming.js';
|
|
|
39
39
|
import { createPeerDataOperationRequester } from '../message/primitives/peer-data-operation.js';
|
|
40
40
|
import { WaMessageClient } from '../message/WaMessageClient.js';
|
|
41
41
|
import { getWaCompanionPlatformId, WA_DEFAULTS, WA_DISCONNECT_REASONS, WA_NEWSLETTER_NOTIFICATION_TAGS, WA_NODE_TAGS, WA_NOTIFICATION_TYPES, WA_PRIVACY_TOKEN_NOTIFICATION_TYPE } from '../protocol/constants.js';
|
|
42
|
-
import { isNewsletterJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
|
|
42
|
+
import { isNewsletterJid, isOwnAccountJid, parseSignalAddressFromJid, toUserJid } from '../protocol/jid.js';
|
|
43
43
|
import { WA_PRESENCE_TYPES } from '../protocol/presence.js';
|
|
44
44
|
import { createOutboundRetryTracker } from '../retry/tracker.js';
|
|
45
45
|
import { SignalDeviceSyncApi } from '../signal/api/SignalDeviceSyncApi.js';
|
|
@@ -483,7 +483,8 @@ export function buildWaClientDependencies(input) {
|
|
|
483
483
|
});
|
|
484
484
|
const presenceCoordinator = createPresenceCoordinator({
|
|
485
485
|
sendNode: (node) => nodeOrchestrator.sendNode(node, false),
|
|
486
|
-
getCurrentCredentials
|
|
486
|
+
getCurrentCredentials,
|
|
487
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid)
|
|
487
488
|
});
|
|
488
489
|
const peerDataOperation = createPeerDataOperationRequester({
|
|
489
490
|
logger,
|
|
@@ -555,9 +556,7 @@ export function buildWaClientDependencies(input) {
|
|
|
555
556
|
const credentials = getCurrentCredentials();
|
|
556
557
|
if (!credentials)
|
|
557
558
|
return false;
|
|
558
|
-
|
|
559
|
-
return ((!!credentials.meJid && toUserJid(credentials.meJid) === candidateUser) ||
|
|
560
|
-
(!!credentials.meLid && toUserJid(credentials.meLid) === candidateUser));
|
|
559
|
+
return isOwnAccountJid(deviceJid, credentials.meJid, credentials.meLid);
|
|
561
560
|
},
|
|
562
561
|
sendKeyShare: (toDeviceJid, keys, missingKeyIds) => messageDispatch.sendAppStateSyncKeyShare(toDeviceJid, keys, missingKeyIds),
|
|
563
562
|
triggerSync: async () => {
|
|
@@ -600,6 +599,7 @@ export function buildWaClientDependencies(input) {
|
|
|
600
599
|
queryLidsByPhoneJids: (phoneJids) => signalDeviceSync.queryLidsByPhoneJids(phoneJids),
|
|
601
600
|
mutations: appStateMutations,
|
|
602
601
|
applyOwnPushName,
|
|
602
|
+
resolvePrivacyTokenNode: (jid) => trustedContactToken.resolveReceiverTokenNode(jid),
|
|
603
603
|
logger
|
|
604
604
|
});
|
|
605
605
|
const statusCoordinator = createStatusCoordinator({
|
|
@@ -680,6 +680,7 @@ export function buildWaClientDependencies(input) {
|
|
|
680
680
|
logger,
|
|
681
681
|
sendNode: runtime.sendNode,
|
|
682
682
|
getMeJid: () => getCurrentCredentials()?.meJid,
|
|
683
|
+
getMeLid: () => getCurrentCredentials()?.meLid,
|
|
683
684
|
signalProtocol,
|
|
684
685
|
senderKeyManager,
|
|
685
686
|
onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
|
|
@@ -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, 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';
|
|
@@ -322,7 +325,8 @@ export class WaMessageDispatchCoordinator {
|
|
|
322
325
|
await this.deps.messageClient.sendReceipt(input);
|
|
323
326
|
}
|
|
324
327
|
async publishProtocolMessageToDevice(deviceJid, protocolMessage, options) {
|
|
325
|
-
const
|
|
328
|
+
const credentials = this.deps.getCurrentCredentials();
|
|
329
|
+
const meJid = credentials?.meJid;
|
|
326
330
|
const meParsed = meJid ? parseJidFull(meJid) : undefined;
|
|
327
331
|
const meUserJid = meParsed?.userJid;
|
|
328
332
|
let senderIcdc = null;
|
|
@@ -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) {
|
|
@@ -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': {
|
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()) {
|
|
@@ -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',
|
|
@@ -9,6 +9,7 @@ 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>;
|
|
@@ -31,6 +31,52 @@ function extractMessageIdentityAttrs(attrs) {
|
|
|
31
31
|
pushName: attrs.notify
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Self-authored 1:1 chat is the recipient, so its alternate addressing is the
|
|
36
|
+
* `recipient*` attrs (the `sender*` attrs describe me). Promotes `recipientAlt`
|
|
37
|
+
* to `remoteJidAlt` and drops the stale sender/recipient fields.
|
|
38
|
+
*/
|
|
39
|
+
function promoteRecipientAddressing(identity) {
|
|
40
|
+
return {
|
|
41
|
+
...(identity.recipientAlt ? { remoteJidAlt: identity.recipientAlt } : {}),
|
|
42
|
+
...(identity.senderUsername !== undefined
|
|
43
|
+
? { senderUsername: identity.senderUsername }
|
|
44
|
+
: {})
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the {@link WaIncomingMessageKey} for a decrypted stanza. `remoteJid`
|
|
49
|
+
* is the chat: the `from`, or the recipient (`recipient` attr, then the
|
|
50
|
+
* deviceSentMessage `destinationJid`) when the message is `fromMe`.
|
|
51
|
+
*/
|
|
52
|
+
function buildIncomingMessageKey(node, sender, options, destinationJid) {
|
|
53
|
+
const fromUserJid = node.attrs.from ? (0, jid_1.toUserJid)(node.attrs.from) : node.attrs.from;
|
|
54
|
+
const isGroup = fromUserJid ? (0, jid_1.isGroupJid)(fromUserJid) : false;
|
|
55
|
+
const isBroadcast = fromUserJid ? (0, jid_1.isBroadcastJid)(fromUserJid) : false;
|
|
56
|
+
const fromMe = sender
|
|
57
|
+
? (0, jid_1.isOwnAccountJid)(sender.userJid, options.getMeJid?.(), options.getMeLid?.())
|
|
58
|
+
: false;
|
|
59
|
+
const selfSentChat = fromMe && !isGroup && !isBroadcast
|
|
60
|
+
? (node.attrs.recipient ?? destinationJid ?? undefined)
|
|
61
|
+
: undefined;
|
|
62
|
+
const chatJid = selfSentChat ? (0, jid_1.toUserJid)(selfSentChat) : fromUserJid;
|
|
63
|
+
const { pushName, ...identity } = extractMessageIdentityAttrs(node.attrs);
|
|
64
|
+
const keyIdentity = selfSentChat ? promoteRecipientAddressing(identity) : identity;
|
|
65
|
+
return {
|
|
66
|
+
pushName,
|
|
67
|
+
key: {
|
|
68
|
+
remoteJid: chatJid ?? '',
|
|
69
|
+
id: node.attrs.id ?? '',
|
|
70
|
+
fromMe,
|
|
71
|
+
isGroup,
|
|
72
|
+
isBroadcast,
|
|
73
|
+
isNewsletter: false,
|
|
74
|
+
...keyIdentity,
|
|
75
|
+
senderDevice: sender?.device ?? 0,
|
|
76
|
+
...((isGroup || isBroadcast) && sender ? { participant: sender.userJid } : {})
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
34
80
|
function pickNextRetryCount(node) {
|
|
35
81
|
const retryNode = (0, helpers_1.findNodeChild)(node, 'retry');
|
|
36
82
|
const parsed = (0, primitives_1.parseOptionalInt)(retryNode?.attrs.count);
|
|
@@ -244,26 +290,11 @@ function processMsmsgEncNode(node, encNode, senderJid, options) {
|
|
|
244
290
|
encPayload: decoded.encPayload
|
|
245
291
|
}
|
|
246
292
|
};
|
|
247
|
-
const chatJid = node.attrs.from ? (0, jid_1.toUserJid)(node.attrs.from) : node.attrs.from;
|
|
248
293
|
const sender = senderJid ? (0, jid_1.parseJidFull)(senderJid) : null;
|
|
249
|
-
const
|
|
250
|
-
const isBroadcast = chatJid ? (0, jid_1.isBroadcastJid)(chatJid) : false;
|
|
251
|
-
const { pushName, ...keyIdentity } = extractMessageIdentityAttrs(node.attrs);
|
|
294
|
+
const { key, pushName } = buildIncomingMessageKey(node, sender ? { userJid: sender.userJid, device: sender.address.device } : null, options);
|
|
252
295
|
options.emitIncomingMessage?.({
|
|
253
296
|
rawNode: buildIncomingEventRawNode(node),
|
|
254
|
-
key
|
|
255
|
-
remoteJid: chatJid ?? '',
|
|
256
|
-
id: node.attrs.id ?? '',
|
|
257
|
-
fromMe: false,
|
|
258
|
-
isGroup,
|
|
259
|
-
isBroadcast,
|
|
260
|
-
isNewsletter: false,
|
|
261
|
-
...keyIdentity,
|
|
262
|
-
...((isGroup || isBroadcast) && sender?.userJid
|
|
263
|
-
? { participant: sender.userJid }
|
|
264
|
-
: {}),
|
|
265
|
-
senderDevice: sender?.address.device ?? 0
|
|
266
|
-
},
|
|
297
|
+
key,
|
|
267
298
|
stanzaType: node.attrs.type,
|
|
268
299
|
offline: node.attrs.offline !== undefined,
|
|
269
300
|
timestampSeconds: (0, primitives_1.parseOptionalInt)(node.attrs.t),
|
|
@@ -317,29 +348,14 @@ async function decryptAndProcessEncNode(node, encNode, encType, senderJid, optio
|
|
|
317
348
|
}
|
|
318
349
|
}
|
|
319
350
|
if (shouldEmitIncomingMessage(message)) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const chatJid = fromAttr ? (0, jid_1.toUserJid)(fromAttr) : fromAttr;
|
|
325
|
-
const isGroup = chatJid ? (0, jid_1.isGroupJid)(chatJid) : false;
|
|
326
|
-
const isBroadcast = chatJid ? (0, jid_1.isBroadcastJid)(chatJid) : false;
|
|
327
|
-
const senderUserJid = `${senderAddress.user}@${senderAddress.server}`;
|
|
328
|
-
const { pushName, ...keyIdentity } = extractMessageIdentityAttrs(node.attrs);
|
|
351
|
+
const { key, pushName } = buildIncomingMessageKey(node, {
|
|
352
|
+
userJid: `${senderAddress.user}@${senderAddress.server}`,
|
|
353
|
+
device: senderAddress.device
|
|
354
|
+
}, options, decodedMessage.deviceSentMessage?.destinationJid ?? undefined);
|
|
329
355
|
const expirationSeconds = (0, context_info_1.pickIncomingExpirationSeconds)(message);
|
|
330
356
|
options.emitIncomingMessage?.({
|
|
331
357
|
rawNode: buildIncomingEventRawNode(node),
|
|
332
|
-
key
|
|
333
|
-
remoteJid: chatJid ?? '',
|
|
334
|
-
id: node.attrs.id ?? '',
|
|
335
|
-
fromMe: false,
|
|
336
|
-
isGroup,
|
|
337
|
-
isBroadcast,
|
|
338
|
-
isNewsletter: false,
|
|
339
|
-
...keyIdentity,
|
|
340
|
-
senderDevice: senderAddress.device,
|
|
341
|
-
...(isGroup || isBroadcast ? { participant: senderUserJid } : {})
|
|
342
|
-
},
|
|
358
|
+
key,
|
|
343
359
|
stanzaType: node.attrs.type,
|
|
344
360
|
offline: node.attrs.offline !== undefined,
|
|
345
361
|
timestampSeconds: (0, primitives_1.parseOptionalInt)(node.attrs.t),
|
|
@@ -418,10 +434,15 @@ async function handleIncomingMessageAck(node, options) {
|
|
|
418
434
|
continue;
|
|
419
435
|
}
|
|
420
436
|
const encType = child.attrs.type === 'msg' ? 'msg' : 'pkmsg';
|
|
421
|
-
result = await decryptAndProcessEncNode(node, child, encType, senderJid, options, (ciphertext, senderAddress) =>
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
437
|
+
result = await decryptAndProcessEncNode(node, child, encType, senderJid, options, (ciphertext, senderAddress) => {
|
|
438
|
+
const sessionJid = (0, jid_1.canonicalizeOwnAccountJid)(senderJid, options.getMeJid?.(), options.getMeLid?.());
|
|
439
|
+
return options.signalProtocol.decryptMessage(sessionJid === senderJid
|
|
440
|
+
? senderAddress
|
|
441
|
+
: (0, jid_1.parseSignalAddressFromJid)(sessionJid), {
|
|
442
|
+
type: encType,
|
|
443
|
+
ciphertext
|
|
444
|
+
});
|
|
445
|
+
});
|
|
425
446
|
break;
|
|
426
447
|
}
|
|
427
448
|
case 'msmsg': {
|
package/dist/protocol/jid.d.ts
CHANGED
|
@@ -66,6 +66,18 @@ export declare function toUserJid(jid: string, options?: {
|
|
|
66
66
|
readonly canonicalizeSignalServer?: boolean;
|
|
67
67
|
readonly hostDomain?: string;
|
|
68
68
|
}): string;
|
|
69
|
+
/**
|
|
70
|
+
* True when `jid` is the account's own user, matching the `meJid` (pn) or
|
|
71
|
+
* `meLid` (lid) identity device-insensitively. Mirrors WhatsApp Web's
|
|
72
|
+
* `isMeAccount`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isOwnAccountJid(jid: string, meJid: string | null | undefined, meLid: string | null | undefined): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Rewrites the account's own PN device JID to its LID equivalent so self traffic
|
|
77
|
+
* keys one LID session instead of forking into separate PN and LID ratchets.
|
|
78
|
+
* No-op for other users, already-LID/unparseable JIDs, or unknown identity.
|
|
79
|
+
*/
|
|
80
|
+
export declare function canonicalizeOwnAccountJid(jid: string, meJid: string | null | undefined, meLid: string | null | undefined): string;
|
|
69
81
|
/**
|
|
70
82
|
* Returns the JID in its full device form. JIDs with `device === 0` lose the
|
|
71
83
|
* device segment (`user@server`); all others keep `user:device@server`.
|
package/dist/protocol/jid.js
CHANGED
|
@@ -15,6 +15,8 @@ exports.parseJidFull = parseJidFull;
|
|
|
15
15
|
exports.canonicalizeSignalServer = canonicalizeSignalServer;
|
|
16
16
|
exports.canonicalizeSignalJid = canonicalizeSignalJid;
|
|
17
17
|
exports.toUserJid = toUserJid;
|
|
18
|
+
exports.isOwnAccountJid = isOwnAccountJid;
|
|
19
|
+
exports.canonicalizeOwnAccountJid = canonicalizeOwnAccountJid;
|
|
18
20
|
exports.normalizeDeviceJid = normalizeDeviceJid;
|
|
19
21
|
exports.applyDeviceToJid = applyDeviceToJid;
|
|
20
22
|
exports.isHostedDeviceId = isHostedDeviceId;
|
|
@@ -217,6 +219,48 @@ function toUserJid(jid, options = {}) {
|
|
|
217
219
|
: address.server;
|
|
218
220
|
return `${address.user}@${server}`;
|
|
219
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* True when `jid` is the account's own user, matching the `meJid` (pn) or
|
|
224
|
+
* `meLid` (lid) identity device-insensitively. Mirrors WhatsApp Web's
|
|
225
|
+
* `isMeAccount`.
|
|
226
|
+
*/
|
|
227
|
+
function isOwnAccountJid(jid, meJid, meLid) {
|
|
228
|
+
const candidateUser = toUserJid(jid);
|
|
229
|
+
return ((!!meJid && toUserJid(meJid) === candidateUser) ||
|
|
230
|
+
(!!meLid && toUserJid(meLid) === candidateUser));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Rewrites the account's own PN device JID to its LID equivalent so self traffic
|
|
234
|
+
* keys one LID session instead of forking into separate PN and LID ratchets.
|
|
235
|
+
* No-op for other users, already-LID/unparseable JIDs, or unknown identity.
|
|
236
|
+
*/
|
|
237
|
+
function canonicalizeOwnAccountJid(jid, meJid, meLid) {
|
|
238
|
+
if (!meJid || !meLid)
|
|
239
|
+
return jid;
|
|
240
|
+
let address;
|
|
241
|
+
try {
|
|
242
|
+
address = parseSignalAddressFromJid(jid);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return jid;
|
|
246
|
+
}
|
|
247
|
+
if ((address.server ?? constants_1.WA_DEFAULTS.HOST_DOMAIN) !== constants_1.WA_DEFAULTS.HOST_DOMAIN)
|
|
248
|
+
return jid;
|
|
249
|
+
let mePnUser;
|
|
250
|
+
let meLidUser;
|
|
251
|
+
try {
|
|
252
|
+
mePnUser = parseSignalAddressFromJid(meJid).user;
|
|
253
|
+
meLidUser = parseSignalAddressFromJid(meLid).user;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return jid;
|
|
257
|
+
}
|
|
258
|
+
if (address.user !== mePnUser)
|
|
259
|
+
return jid;
|
|
260
|
+
return address.device === 0
|
|
261
|
+
? `${meLidUser}@${constants_1.WA_DEFAULTS.LID_SERVER}`
|
|
262
|
+
: `${meLidUser}:${address.device}@${constants_1.WA_DEFAULTS.LID_SERVER}`;
|
|
263
|
+
}
|
|
220
264
|
/**
|
|
221
265
|
* Returns the JID in its full device form. JIDs with `device === 0` lose the
|
|
222
266
|
* device segment (`user@server`); all others keep `user:device@server`.
|
package/dist/retry/reason.js
CHANGED
|
@@ -20,10 +20,10 @@ const RETRY_REASON_MATCHERS = [
|
|
|
20
20
|
},
|
|
21
21
|
{ matches: ['invalid signature'], code: constants_1.RETRY_REASON.SignalErrorInvalidSignature },
|
|
22
22
|
{
|
|
23
|
-
matches: ['too many messages in future', 'future message'],
|
|
23
|
+
matches: ['too far in future', 'too many messages in future', 'future message'],
|
|
24
24
|
code: constants_1.RETRY_REASON.SignalErrorFutureMessage
|
|
25
25
|
},
|
|
26
|
-
{ matches: ['invalid mac'], code: constants_1.RETRY_REASON.SignalErrorBadMac },
|
|
26
|
+
{ matches: ['invalid message mac', 'invalid mac'], code: constants_1.RETRY_REASON.SignalErrorBadMac },
|
|
27
27
|
{ matches: ['invalid session'], code: constants_1.RETRY_REASON.SignalErrorInvalidSession },
|
|
28
28
|
{ matches: ['invalid message key'], code: constants_1.RETRY_REASON.SignalErrorInvalidMsgKey },
|
|
29
29
|
{
|
|
@@ -24,8 +24,12 @@ class WaNodeOrchestrator {
|
|
|
24
24
|
return this.pendingQueries.size > 0;
|
|
25
25
|
}
|
|
26
26
|
clearPending(reason) {
|
|
27
|
-
this.
|
|
28
|
-
|
|
27
|
+
const count = this.pendingQueries.size;
|
|
28
|
+
if (count === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.logger.debug('clearing pending node queries', {
|
|
32
|
+
count,
|
|
29
33
|
reason: reason.message
|
|
30
34
|
});
|
|
31
35
|
for (const pending of this.pendingQueries.values()) {
|
|
@@ -9,5 +9,11 @@ export interface BuildPresenceSubscribeNodeInput {
|
|
|
9
9
|
readonly jid: string;
|
|
10
10
|
readonly name?: string;
|
|
11
11
|
readonly context?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Receiver-mode `<tctoken>` node echoed back to prove this account is a
|
|
14
|
+
* trusted contact, gating the target's presence/last-seen visibility.
|
|
15
|
+
* Attached as a child of the `<presence>` stanza when present.
|
|
16
|
+
*/
|
|
17
|
+
readonly privacyTokenNode?: BinaryNode;
|
|
12
18
|
}
|
|
13
19
|
export declare function buildPresenceSubscribeNode(input: BuildPresenceSubscribeNodeInput): BinaryNode;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BinaryNode } from '../../types';
|
|
2
2
|
export type WaProfilePictureType = 'preview' | 'image';
|
|
3
|
-
export declare function buildGetProfilePictureIq(targetJid: string, type?: WaProfilePictureType, existingId?: string): BinaryNode;
|
|
3
|
+
export declare function buildGetProfilePictureIq(targetJid: string, type?: WaProfilePictureType, existingId?: string, privacyTokenNode?: BinaryNode): BinaryNode;
|
|
4
4
|
export declare function buildSetProfilePictureIq(imageBytes: Uint8Array, targetJid?: string): BinaryNode;
|
|
5
5
|
export declare function buildDeleteProfilePictureIq(targetJid?: string): BinaryNode;
|
|
6
6
|
export declare function buildSetStatusIq(text: string): BinaryNode;
|
|
@@ -11,7 +11,7 @@ exports.buildGetUsernameUsyncQueryNode = buildGetUsernameUsyncQueryNode;
|
|
|
11
11
|
exports.buildGetStatusUsyncQueryNodes = buildGetStatusUsyncQueryNodes;
|
|
12
12
|
const constants_1 = require("../../../protocol/constants");
|
|
13
13
|
const query_1 = require("../../node/query");
|
|
14
|
-
function buildGetProfilePictureIq(targetJid, type = 'preview', existingId) {
|
|
14
|
+
function buildGetProfilePictureIq(targetJid, type = 'preview', existingId, privacyTokenNode) {
|
|
15
15
|
const pictureAttrs = {
|
|
16
16
|
type,
|
|
17
17
|
query: 'url'
|
|
@@ -22,7 +22,8 @@ function buildGetProfilePictureIq(targetJid, type = 'preview', existingId) {
|
|
|
22
22
|
return (0, query_1.buildIqNode)(constants_1.WA_IQ_TYPES.GET, constants_1.WA_DEFAULTS.HOST_DOMAIN, constants_1.WA_XMLNS.PROFILE_PICTURE, [
|
|
23
23
|
{
|
|
24
24
|
tag: constants_1.WA_NODE_TAGS.PICTURE,
|
|
25
|
-
attrs: pictureAttrs
|
|
25
|
+
attrs: pictureAttrs,
|
|
26
|
+
...(privacyTokenNode ? { content: [privacyTokenNode] } : {})
|
|
26
27
|
}
|
|
27
28
|
], {
|
|
28
29
|
target: targetJid
|
|
@@ -76,11 +76,9 @@ function buildRetryReceiptNode(input) {
|
|
|
76
76
|
v: constants_2.RETRY_RECEIPT_VERSION,
|
|
77
77
|
count: String(input.retryCount),
|
|
78
78
|
id: input.originalMsgId,
|
|
79
|
-
t: input.t
|
|
79
|
+
t: input.t,
|
|
80
|
+
error: String(input.error ?? 0)
|
|
80
81
|
};
|
|
81
|
-
if (input.error !== undefined && input.error !== 0) {
|
|
82
|
-
retryAttrs.error = String(input.error);
|
|
83
|
-
}
|
|
84
82
|
const content = [
|
|
85
83
|
{
|
|
86
84
|
tag: 'retry',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zapo-js",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "High-performance WhatsApp Web TypeScript library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vinikjkkj <contact@vinicius.email> (https://github.com/vinikjkkj)",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
},
|
|
125
125
|
"scripts": {
|
|
126
126
|
"preinstall": "node ./scripts/check-node-version.cjs",
|
|
127
|
-
"
|
|
127
|
+
"prepare": "npm run build",
|
|
128
128
|
"changeset": "changeset",
|
|
129
129
|
"changeset:status": "changeset status --verbose",
|
|
130
130
|
"version:packages": "changeset version",
|