@zapo-js/fake-server 1.0.0 → 1.1.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/README.md +93 -5
- package/dist/api/FakePeer.js +7 -1
- package/dist/api/FakeServerSession.d.ts +67 -0
- package/dist/api/FakeServerSession.js +293 -0
- package/dist/api/FakeWaServer.d.ts +74 -38
- package/dist/api/FakeWaServer.js +125 -276
- package/dist/api/iq-handlers.d.ts +1 -0
- package/dist/api/iq-handlers.js +23 -0
- package/dist/bin.js +69 -21
- package/dist/esm/api/FakePeer.js +7 -1
- package/dist/esm/api/FakeServerSession.js +287 -0
- package/dist/esm/api/FakeWaServer.js +123 -275
- package/dist/esm/api/iq-handlers.js +23 -0
- package/dist/esm/bin.js +67 -19
- package/dist/esm/index.js +1 -1
- package/dist/esm/infra/WaFakeConnectionPipeline.js +21 -9
- package/dist/esm/protocol/auth/cert-chain.js +13 -2
- package/dist/esm/protocol/iq/router.js +8 -2
- package/dist/esm/transport/util.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/infra/WaFakeConnectionPipeline.d.ts +10 -2
- package/dist/infra/WaFakeConnectionPipeline.js +21 -9
- package/dist/protocol/auth/cert-chain.d.ts +4 -0
- package/dist/protocol/auth/cert-chain.js +13 -2
- package/dist/protocol/iq/router.d.ts +7 -1
- package/dist/protocol/iq/router.js +8 -2
- package/dist/transport/util.d.ts +1 -1
- package/dist/transport/util.js +2 -1
- package/package.json +6 -1
|
@@ -3,122 +3,92 @@ import { WaFakeConnectionPipeline } from '../infra/WaFakeConnectionPipeline.js';
|
|
|
3
3
|
import { WaFakeMediaHttpsServer } from '../infra/WaFakeMediaHttpsServer.js';
|
|
4
4
|
import { WaFakeWsServer } from '../infra/WaFakeWsServer.js';
|
|
5
5
|
import { generateFakeNoiseRootCa } from '../protocol/auth/cert-chain.js';
|
|
6
|
-
import { WaFakeIqRouter } from '../protocol/iq/router.js';
|
|
7
6
|
import { FakeMediaStore } from '../state/fake-media-store.js';
|
|
8
7
|
import { X25519 } from '../transport/crypto.js';
|
|
9
|
-
import { AppStateSyncManager } from './AppStateSyncManager.js';
|
|
10
8
|
import { FakePairingDriver } from './FakePairingDriver.js';
|
|
11
9
|
import { FakePeer } from './FakePeer.js';
|
|
12
|
-
import {
|
|
13
|
-
import { PreKeyDispenser } from './PreKeyDispenser.js';
|
|
10
|
+
import { FakeServerSession } from './FakeServerSession.js';
|
|
14
11
|
import { Scenario } from './Scenario.js';
|
|
15
|
-
|
|
12
|
+
export { FakeServerSession } from './FakeServerSession.js';
|
|
16
13
|
export class FakeWaServer {
|
|
17
14
|
constructor(options = {}) {
|
|
18
|
-
this.registries = new ServerRegistries();
|
|
19
|
-
this.preKeyDispenser = new PreKeyDispenser();
|
|
20
|
-
this.appStateSync = new AppStateSyncManager();
|
|
21
15
|
this.pipelines = new Set();
|
|
22
|
-
this.
|
|
23
|
-
this.capturedStanzas = [];
|
|
24
|
-
this.pendingIqExpectations = new Set();
|
|
25
|
-
this.pendingStanzaExpectations = new Set();
|
|
16
|
+
this.pipelineSessions = new WeakMap();
|
|
26
17
|
this.authenticatedListeners = new Set();
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
18
|
+
this.sessions = new Map();
|
|
19
|
+
this.globalIqHandlers = new Set();
|
|
29
20
|
this.rootCa = null;
|
|
30
21
|
this.serverStaticKeyPair = null;
|
|
31
22
|
this.listenInfo = null;
|
|
32
|
-
this.
|
|
23
|
+
this.pipelineListeners = new Set();
|
|
33
24
|
this.rejectMode = null;
|
|
34
25
|
this.mediaStore = new FakeMediaStore();
|
|
35
26
|
this.mediaHttpsServer = new WaFakeMediaHttpsServer();
|
|
36
27
|
this.capturedMediaUploads = [];
|
|
37
28
|
this.nextUploadCounter = 0;
|
|
38
29
|
this.cachedMediaProxyAgent = null;
|
|
30
|
+
this.options = options;
|
|
39
31
|
this.wsServer = new WaFakeWsServer(options);
|
|
40
32
|
this.wsServer.onConnection((connection) => this.handleConnection(connection));
|
|
41
|
-
|
|
33
|
+
this.defaultSession = this.createSession('__default__');
|
|
42
34
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
lookupDeviceIdsForUser: (userJid) => reg.lookupDeviceIdsForUser(userJid),
|
|
79
|
-
notifyGroupOp: (op) => reg.notifyGroupOp(op),
|
|
80
|
-
mutatePrivacySettings: (category, value) => reg.mutatePrivacySettings(category, value),
|
|
81
|
-
mutateBlocklist: (action, jid) => reg.mutateBlocklist(action, jid),
|
|
82
|
-
notifyProfilePictureSet: (op) => reg.notifyProfilePictureSet(op),
|
|
83
|
-
handleProfilePictureSet: (targetJid, newId) => reg.handleProfilePictureSet(targetJid, newId),
|
|
84
|
-
notifyStatusSet: (text) => reg.notifyStatusSet(text),
|
|
85
|
-
notifyLogout: () => reg.notifyLogout(),
|
|
86
|
-
notifyPrivacyTokenIssue: (token) => reg.notifyPrivacyTokenIssue(token),
|
|
87
|
-
notifyDirtyBitsClear: (op) => reg.notifyDirtyBitsClear(op),
|
|
88
|
-
notifyPrivacySet: (change) => {
|
|
89
|
-
for (const listener of reg.privacySetListeners) {
|
|
90
|
-
try {
|
|
91
|
-
listener(change);
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
void error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
notifyBlocklistChange: (change) => {
|
|
99
|
-
for (const listener of reg.blocklistChangeListeners) {
|
|
100
|
-
try {
|
|
101
|
-
listener(change);
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
void error;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
capturePreKeyBundle: (bundle) => preKey.captureBundle(bundle),
|
|
109
|
-
consumeOutboundAppStatePatches: (iq) => appState.consumeOutboundAppStatePatches(iq),
|
|
110
|
-
get appStateCollectionProviders() {
|
|
111
|
-
return appState.appStateCollectionProviders;
|
|
112
|
-
},
|
|
113
|
-
requireMediaHttpsInfo: () => this.requireMediaHttpsInfo()
|
|
114
|
-
};
|
|
35
|
+
/** State of the single default session (the only session for one client). */
|
|
36
|
+
get registries() {
|
|
37
|
+
return this.defaultSession.registries;
|
|
38
|
+
}
|
|
39
|
+
get preKeyDispenser() {
|
|
40
|
+
return this.defaultSession.preKeyDispenser;
|
|
41
|
+
}
|
|
42
|
+
get appStateSync() {
|
|
43
|
+
return this.defaultSession.appStateSync;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns the isolated session for `id`, creating it on first use. Every
|
|
47
|
+
* session gets its own registries, prekey dispenser, app-state, IQ router,
|
|
48
|
+
* and stanza capture. Use with the `sessionKey` option to run several
|
|
49
|
+
* `WaClient`s against one server without them sharing any state.
|
|
50
|
+
*/
|
|
51
|
+
session(id) {
|
|
52
|
+
const existing = this.sessions.get(id);
|
|
53
|
+
if (existing) {
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
return this.createSession(id);
|
|
57
|
+
}
|
|
58
|
+
/** The session a connection is bound to (default before authentication). */
|
|
59
|
+
sessionFor(pipeline) {
|
|
60
|
+
return this.pipelineSessions.get(pipeline) ?? this.defaultSession;
|
|
61
|
+
}
|
|
62
|
+
createSession(id) {
|
|
63
|
+
const session = new FakeServerSession(id, { requireMediaHttpsInfo: () => this.requireMediaHttpsInfo() }, { defaultIqHandlers: this.options.defaultIqHandlers !== false });
|
|
64
|
+
this.sessions.set(id, session);
|
|
65
|
+
// Server-level custom handlers apply to every session, present and future.
|
|
66
|
+
for (const handler of this.globalIqHandlers) {
|
|
67
|
+
handler.unregisterBySession.set(session, session.registerIqHandler(handler.matcher, handler.respond, handler.label));
|
|
68
|
+
}
|
|
69
|
+
return session;
|
|
115
70
|
}
|
|
116
71
|
registerIqHandler(matcher, respond, label) {
|
|
117
|
-
const
|
|
118
|
-
|
|
72
|
+
const entry = {
|
|
73
|
+
matcher,
|
|
74
|
+
respond,
|
|
75
|
+
label,
|
|
76
|
+
unregisterBySession: new Map()
|
|
77
|
+
};
|
|
78
|
+
for (const session of this.sessions.values()) {
|
|
79
|
+
entry.unregisterBySession.set(session, session.registerIqHandler(matcher, respond, label));
|
|
80
|
+
}
|
|
81
|
+
this.globalIqHandlers.add(entry);
|
|
82
|
+
return () => {
|
|
83
|
+
for (const unregister of entry.unregisterBySession.values()) {
|
|
84
|
+
unregister();
|
|
85
|
+
}
|
|
86
|
+
entry.unregisterBySession.clear();
|
|
87
|
+
this.globalIqHandlers.delete(entry);
|
|
88
|
+
};
|
|
119
89
|
}
|
|
120
90
|
async routeIqForTest(iq) {
|
|
121
|
-
return this.
|
|
91
|
+
return this.defaultSession.routeIq(iq);
|
|
122
92
|
}
|
|
123
93
|
onOutboundGroupOp(listener) {
|
|
124
94
|
return this.registries.onOutboundGroupOp(listener);
|
|
@@ -175,7 +145,7 @@ export class FakeWaServer {
|
|
|
175
145
|
return this.registries.createFakeGroup(input);
|
|
176
146
|
}
|
|
177
147
|
async triggerPreKeyUpload(pipeline, options = {}) {
|
|
178
|
-
return this.preKeyDispenser.triggerPreKeyUpload(pipeline, options);
|
|
148
|
+
return this.sessionFor(pipeline).preKeyDispenser.triggerPreKeyUpload(pipeline, options);
|
|
179
149
|
}
|
|
180
150
|
awaitPreKeyBundle(timeoutMs = 15000) {
|
|
181
151
|
return this.preKeyDispenser.awaitPreKeyBundle(timeoutMs);
|
|
@@ -205,7 +175,7 @@ export class FakeWaServer {
|
|
|
205
175
|
return this.appStateSync.provideAppStateCollection(name, provider);
|
|
206
176
|
}
|
|
207
177
|
async pushServerSyncNotification(pipeline, input) {
|
|
208
|
-
return this.appStateSync.pushServerSyncNotification(pipeline, input);
|
|
178
|
+
return this.sessionFor(pipeline).appStateSync.pushServerSyncNotification(pipeline, input);
|
|
209
179
|
}
|
|
210
180
|
onAuthenticatedPipeline(listener) {
|
|
211
181
|
this.authenticatedListeners.add(listener);
|
|
@@ -215,78 +185,24 @@ export class FakeWaServer {
|
|
|
215
185
|
configure(new Scenario(this));
|
|
216
186
|
}
|
|
217
187
|
expectIq(matcher, options = {}) {
|
|
218
|
-
|
|
219
|
-
for (const captured of this.capturedStanzas) {
|
|
220
|
-
if (matchesIq(captured, matcher)) {
|
|
221
|
-
return Promise.resolve(captured);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return new Promise((resolve, reject) => {
|
|
225
|
-
const expectation = {
|
|
226
|
-
matcher,
|
|
227
|
-
resolve: (iq) => {
|
|
228
|
-
this.pendingIqExpectations.delete(expectation);
|
|
229
|
-
clearTimeout(expectation.timer);
|
|
230
|
-
resolve(iq);
|
|
231
|
-
},
|
|
232
|
-
reject: (error) => {
|
|
233
|
-
this.pendingIqExpectations.delete(expectation);
|
|
234
|
-
clearTimeout(expectation.timer);
|
|
235
|
-
reject(error);
|
|
236
|
-
},
|
|
237
|
-
timer: setTimeout(() => {
|
|
238
|
-
this.pendingIqExpectations.delete(expectation);
|
|
239
|
-
reject(new Error(`expectIq timed out after ${timeoutMs}ms (${describeMatcher(matcher)})`));
|
|
240
|
-
}, timeoutMs)
|
|
241
|
-
};
|
|
242
|
-
this.pendingIqExpectations.add(expectation);
|
|
243
|
-
});
|
|
188
|
+
return this.defaultSession.expectIq(matcher, options);
|
|
244
189
|
}
|
|
245
190
|
capturedStanzaSnapshot() {
|
|
246
|
-
return this.
|
|
191
|
+
return this.defaultSession.capturedStanzaSnapshot();
|
|
247
192
|
}
|
|
248
193
|
/**
|
|
249
|
-
* Subscribes to every stanza captured from the client side (the lib)
|
|
250
|
-
* The listener is called synchronously for each new
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* `expectStanza(...)` per iteration.
|
|
194
|
+
* Subscribes to every stanza captured from the client side (the lib) of
|
|
195
|
+
* the default session. The listener is called synchronously for each new
|
|
196
|
+
* stanza as it arrives. Returns an unsubscribe function. Useful for
|
|
197
|
+
* benches that need to count or react to a stream of receipts/messages
|
|
198
|
+
* without paying the O(N²) cost of polling `capturedStanzaSnapshot()` or
|
|
199
|
+
* queuing one `expectStanza(...)` per iteration.
|
|
256
200
|
*/
|
|
257
201
|
onCapturedStanza(listener) {
|
|
258
|
-
this.
|
|
259
|
-
return () => {
|
|
260
|
-
this.capturedStanzaListeners.delete(listener);
|
|
261
|
-
};
|
|
202
|
+
return this.defaultSession.onCapturedStanza(listener);
|
|
262
203
|
}
|
|
263
204
|
expectStanza(matcher, options = {}) {
|
|
264
|
-
|
|
265
|
-
for (const captured of this.capturedStanzas) {
|
|
266
|
-
if (matchesStanza(captured, matcher)) {
|
|
267
|
-
return Promise.resolve(captured);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return new Promise((resolve, reject) => {
|
|
271
|
-
const expectation = {
|
|
272
|
-
matcher,
|
|
273
|
-
resolve: (node) => {
|
|
274
|
-
this.pendingStanzaExpectations.delete(expectation);
|
|
275
|
-
clearTimeout(expectation.timer);
|
|
276
|
-
resolve(node);
|
|
277
|
-
},
|
|
278
|
-
reject: (error) => {
|
|
279
|
-
this.pendingStanzaExpectations.delete(expectation);
|
|
280
|
-
clearTimeout(expectation.timer);
|
|
281
|
-
reject(error);
|
|
282
|
-
},
|
|
283
|
-
timer: setTimeout(() => {
|
|
284
|
-
this.pendingStanzaExpectations.delete(expectation);
|
|
285
|
-
reject(new Error(`expectStanza timed out after ${timeoutMs}ms (${describeStanzaMatcher(matcher)})`));
|
|
286
|
-
}, timeoutMs)
|
|
287
|
-
};
|
|
288
|
-
this.pendingStanzaExpectations.add(expectation);
|
|
289
|
-
});
|
|
205
|
+
return this.defaultSession.expectStanza(matcher, options);
|
|
290
206
|
}
|
|
291
207
|
async broadcastStanza(node) {
|
|
292
208
|
const tasks = [];
|
|
@@ -348,8 +264,9 @@ export class FakeWaServer {
|
|
|
348
264
|
return this.capturedMediaUploads.slice();
|
|
349
265
|
}
|
|
350
266
|
async createFakePeer(options, pipeline) {
|
|
351
|
-
const
|
|
352
|
-
this.
|
|
267
|
+
const session = this.sessionFor(pipeline);
|
|
268
|
+
const peer = await FakePeer.create(options, this.buildFakePeerDeps(pipeline, session));
|
|
269
|
+
session.registries.registerPeer(peer);
|
|
353
270
|
return peer;
|
|
354
271
|
}
|
|
355
272
|
async createFakePeerWithDevices(input, pipeline) {
|
|
@@ -362,6 +279,7 @@ export class FakeWaServer {
|
|
|
362
279
|
}
|
|
363
280
|
const userPart = input.userJid.slice(0, atIdx);
|
|
364
281
|
const server = input.userJid.slice(atIdx + 1);
|
|
282
|
+
const session = this.sessionFor(pipeline);
|
|
365
283
|
const peers = [];
|
|
366
284
|
for (const deviceId of input.deviceIds) {
|
|
367
285
|
const deviceJid = deviceId === 0 ? input.userJid : `${userPart}:${deviceId}@${server}`;
|
|
@@ -369,36 +287,27 @@ export class FakeWaServer {
|
|
|
369
287
|
jid: deviceJid,
|
|
370
288
|
displayName: input.displayName,
|
|
371
289
|
skipOneTimePreKey: input.skipOneTimePreKey
|
|
372
|
-
}, this.buildFakePeerDeps(pipeline));
|
|
373
|
-
|
|
290
|
+
}, this.buildFakePeerDeps(pipeline, session));
|
|
291
|
+
session.registries.registerPeer(peer);
|
|
374
292
|
peers.push(peer);
|
|
375
293
|
}
|
|
376
294
|
return peers;
|
|
377
295
|
}
|
|
378
|
-
buildFakePeerDeps(pipeline) {
|
|
296
|
+
buildFakePeerDeps(pipeline, session) {
|
|
379
297
|
return {
|
|
380
|
-
bundleResolver: () =>
|
|
381
|
-
reserveOneTimePreKey: () =>
|
|
298
|
+
bundleResolver: () => session.preKeyDispenser.awaitPreKeyBundle(),
|
|
299
|
+
reserveOneTimePreKey: () => session.preKeyDispenser.dispenseOneTimePreKey(),
|
|
382
300
|
pushStanza: (stanza) => pipeline.sendStanza(stanza),
|
|
383
|
-
subscribeInboundMessages: (listener) =>
|
|
384
|
-
const wrapped = (node) => {
|
|
385
|
-
if (node.tag !== 'message')
|
|
386
|
-
return;
|
|
387
|
-
listener(node);
|
|
388
|
-
};
|
|
389
|
-
this.inboundStanzaListeners.add(wrapped);
|
|
390
|
-
return () => {
|
|
391
|
-
this.inboundStanzaListeners.delete(wrapped);
|
|
392
|
-
};
|
|
393
|
-
}
|
|
301
|
+
subscribeInboundMessages: (listener) => session.subscribeInboundMessages(listener)
|
|
394
302
|
};
|
|
395
303
|
}
|
|
396
304
|
async runPairing(pipeline, options, companionMaterialResolver) {
|
|
305
|
+
const session = this.sessionFor(pipeline);
|
|
397
306
|
const driver = new FakePairingDriver(options, {
|
|
398
307
|
pipeline,
|
|
399
308
|
companionMaterialResolver,
|
|
400
309
|
waitForPairDeviceAck: async (pairDeviceIqId) => {
|
|
401
|
-
await
|
|
310
|
+
await session.expectIq({
|
|
402
311
|
id: pairDeviceIqId
|
|
403
312
|
}, {
|
|
404
313
|
timeoutMs: 15000
|
|
@@ -435,8 +344,15 @@ export class FakeWaServer {
|
|
|
435
344
|
const root = this.requireRootCa();
|
|
436
345
|
return { publicKey: root.publicKey, serial: root.serial };
|
|
437
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Subscribes to every new connection pipeline (pre-auth). Listeners
|
|
349
|
+
* fan out; returns an unsubscribe function.
|
|
350
|
+
*/
|
|
438
351
|
onPipeline(listener) {
|
|
439
|
-
this.
|
|
352
|
+
this.pipelineListeners.add(listener);
|
|
353
|
+
return () => {
|
|
354
|
+
this.pipelineListeners.delete(listener);
|
|
355
|
+
};
|
|
440
356
|
}
|
|
441
357
|
async listen() {
|
|
442
358
|
if (this.listenInfo) {
|
|
@@ -478,53 +394,51 @@ export class FakeWaServer {
|
|
|
478
394
|
connection,
|
|
479
395
|
rootCa: this.rootCa,
|
|
480
396
|
serverStaticKeyPair: this.serverStaticKeyPair,
|
|
481
|
-
|
|
397
|
+
routeIq: (node) => this.sessionFor(pipeline).routeIq(node),
|
|
398
|
+
...(this.options.successNodeAttributes !== undefined
|
|
399
|
+
? { successNodeAttributes: this.options.successNodeAttributes }
|
|
400
|
+
: {})
|
|
482
401
|
});
|
|
483
402
|
this.pipelines.add(pipeline);
|
|
403
|
+
this.pipelineSessions.set(pipeline, this.defaultSession);
|
|
484
404
|
pipeline.setEvents({
|
|
485
405
|
onAuthenticated: () => {
|
|
406
|
+
this.bindPipelineSession(pipeline);
|
|
486
407
|
for (const listener of this.authenticatedListeners) {
|
|
487
|
-
|
|
408
|
+
try {
|
|
409
|
+
void Promise.resolve(listener(pipeline)).catch(() => undefined);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
/* ignore */
|
|
413
|
+
}
|
|
488
414
|
}
|
|
489
415
|
},
|
|
490
|
-
onStanza: (node) => this.handleCapturedStanza(node),
|
|
416
|
+
onStanza: (node) => this.sessionFor(pipeline).handleCapturedStanza(node),
|
|
491
417
|
onClose: () => this.pipelines.delete(pipeline)
|
|
492
418
|
});
|
|
493
|
-
this.
|
|
494
|
-
}
|
|
495
|
-
handleCapturedStanza(node) {
|
|
496
|
-
this.capturedStanzas.push(node);
|
|
497
|
-
for (const listener of this.capturedStanzaListeners) {
|
|
419
|
+
for (const listener of this.pipelineListeners) {
|
|
498
420
|
try {
|
|
499
|
-
listener(
|
|
421
|
+
void Promise.resolve(listener(pipeline)).catch(() => undefined);
|
|
500
422
|
}
|
|
501
|
-
catch
|
|
502
|
-
|
|
423
|
+
catch {
|
|
424
|
+
/* ignore */
|
|
503
425
|
}
|
|
504
426
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
if (node.tag !== 'iq') {
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Rebinds a connection to its keyed session once authenticated. Runs
|
|
430
|
+
* before user `onAuthenticated` listeners and before any client stanza, so
|
|
431
|
+
* captures and IQ routing land in the right session from the first frame.
|
|
432
|
+
* No-op without a `sessionKey` resolver (single default session).
|
|
433
|
+
*/
|
|
434
|
+
bindPipelineSession(pipeline) {
|
|
435
|
+
const resolveKey = this.options.sessionKey;
|
|
436
|
+
const clientPayload = pipeline.clientPayload;
|
|
437
|
+
if (!resolveKey || !clientPayload) {
|
|
520
438
|
return;
|
|
521
439
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
expectation.resolve(node);
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
440
|
+
const id = resolveKey({ clientPayload, pipeline });
|
|
441
|
+
this.pipelineSessions.set(pipeline, this.session(id));
|
|
528
442
|
}
|
|
529
443
|
buildMediaRequestHandler() {
|
|
530
444
|
return (req, res) => {
|
|
@@ -612,69 +526,3 @@ function parseQueryParam(query, name) {
|
|
|
612
526
|
}
|
|
613
527
|
return undefined;
|
|
614
528
|
}
|
|
615
|
-
function matchesIq(iq, matcher) {
|
|
616
|
-
if (iq.tag !== 'iq')
|
|
617
|
-
return false;
|
|
618
|
-
if (matcher.id !== undefined && iq.attrs.id !== matcher.id)
|
|
619
|
-
return false;
|
|
620
|
-
if (matcher.type !== undefined && iq.attrs.type !== matcher.type)
|
|
621
|
-
return false;
|
|
622
|
-
if (matcher.xmlns !== undefined && iq.attrs.xmlns !== matcher.xmlns)
|
|
623
|
-
return false;
|
|
624
|
-
if (matcher.childTag !== undefined) {
|
|
625
|
-
const children = Array.isArray(iq.content) ? iq.content : null;
|
|
626
|
-
if (!children || children.length === 0)
|
|
627
|
-
return false;
|
|
628
|
-
if (children[0].tag !== matcher.childTag)
|
|
629
|
-
return false;
|
|
630
|
-
}
|
|
631
|
-
return true;
|
|
632
|
-
}
|
|
633
|
-
function describeMatcher(matcher) {
|
|
634
|
-
const parts = [];
|
|
635
|
-
if (matcher.id !== undefined)
|
|
636
|
-
parts.push(`id=${matcher.id}`);
|
|
637
|
-
if (matcher.type !== undefined)
|
|
638
|
-
parts.push(`type=${matcher.type}`);
|
|
639
|
-
if (matcher.xmlns !== undefined)
|
|
640
|
-
parts.push(`xmlns=${matcher.xmlns}`);
|
|
641
|
-
if (matcher.childTag !== undefined)
|
|
642
|
-
parts.push(`childTag=${matcher.childTag}`);
|
|
643
|
-
return parts.length > 0 ? parts.join(', ') : 'any iq';
|
|
644
|
-
}
|
|
645
|
-
function matchesStanza(node, matcher) {
|
|
646
|
-
if (matcher.tag !== undefined && node.tag !== matcher.tag)
|
|
647
|
-
return false;
|
|
648
|
-
if (matcher.type !== undefined && node.attrs.type !== matcher.type)
|
|
649
|
-
return false;
|
|
650
|
-
if (matcher.xmlns !== undefined && node.attrs.xmlns !== matcher.xmlns)
|
|
651
|
-
return false;
|
|
652
|
-
if (matcher.from !== undefined && node.attrs.from !== matcher.from)
|
|
653
|
-
return false;
|
|
654
|
-
if (matcher.to !== undefined && node.attrs.to !== matcher.to)
|
|
655
|
-
return false;
|
|
656
|
-
if (matcher.childTag !== undefined) {
|
|
657
|
-
const children = Array.isArray(node.content) ? node.content : null;
|
|
658
|
-
if (!children || children.length === 0)
|
|
659
|
-
return false;
|
|
660
|
-
if (children[0].tag !== matcher.childTag)
|
|
661
|
-
return false;
|
|
662
|
-
}
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
|
-
function describeStanzaMatcher(matcher) {
|
|
666
|
-
const parts = [];
|
|
667
|
-
if (matcher.tag !== undefined)
|
|
668
|
-
parts.push(`tag=${matcher.tag}`);
|
|
669
|
-
if (matcher.type !== undefined)
|
|
670
|
-
parts.push(`type=${matcher.type}`);
|
|
671
|
-
if (matcher.xmlns !== undefined)
|
|
672
|
-
parts.push(`xmlns=${matcher.xmlns}`);
|
|
673
|
-
if (matcher.from !== undefined)
|
|
674
|
-
parts.push(`from=${matcher.from}`);
|
|
675
|
-
if (matcher.to !== undefined)
|
|
676
|
-
parts.push(`to=${matcher.to}`);
|
|
677
|
-
if (matcher.childTag !== undefined)
|
|
678
|
-
parts.push(`childTag=${matcher.childTag}`);
|
|
679
|
-
return parts.length > 0 ? parts.join(', ') : 'any stanza';
|
|
680
|
-
}
|
|
@@ -93,6 +93,29 @@ export function registerDefaultIqHandlers(router, deps) {
|
|
|
93
93
|
matcher: { xmlns: 'encrypt', type: 'get', childTag: 'digest' },
|
|
94
94
|
respond: (iq) => buildIqError(iq, { code: 404, text: 'item-not-found' })
|
|
95
95
|
});
|
|
96
|
+
// Baileys-family and whatsmeow clients block on this query before
|
|
97
|
+
// reporting the connection as open; zapo-js uses <digest> instead.
|
|
98
|
+
router.register({
|
|
99
|
+
label: 'prekey-count',
|
|
100
|
+
matcher: { xmlns: 'encrypt', type: 'get', childTag: 'count' },
|
|
101
|
+
respond: (iq) => {
|
|
102
|
+
const result = buildIqResult(iq);
|
|
103
|
+
return {
|
|
104
|
+
...result,
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
tag: 'count',
|
|
108
|
+
attrs: { value: String(deps.countServerPreKeys()) }
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
router.register({
|
|
115
|
+
label: 'passive-mode',
|
|
116
|
+
matcher: { xmlns: 'passive', type: 'set' },
|
|
117
|
+
respond: (iq) => buildIqResult(iq)
|
|
118
|
+
});
|
|
96
119
|
router.register({
|
|
97
120
|
label: 'media-conn',
|
|
98
121
|
matcher: { xmlns: 'w:m', type: 'set', childTag: 'media_conn' },
|