zapo-js 1.6.2 → 1.7.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/abprops-spec.d.ts +10 -0
- package/dist/abprops-spec.js +25 -0
- package/dist/client/WaClient.d.ts +11 -0
- package/dist/client/WaClient.js +60 -0
- package/dist/client/WaClientFactory.d.ts +2 -2
- package/dist/client/WaClientFactory.js +50 -12
- package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
- package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
- package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
- package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
- package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
- package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
- package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
- package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
- package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
- package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
- package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/client/events/dirty.d.ts +3 -0
- package/dist/client/events/dirty.js +12 -7
- package/dist/client/events/offline.d.ts +9 -0
- package/dist/client/events/offline.js +95 -0
- package/dist/client/events/privacy.d.ts +45 -0
- package/dist/client/events/privacy.js +130 -0
- package/dist/client/messaging/group-metadata.d.ts +12 -0
- package/dist/client/messaging/group-metadata.js +24 -8
- package/dist/client/messaging/link-preview.d.ts +7 -0
- package/dist/client/messaging/link-preview.js +27 -0
- package/dist/client/newsletter/content.d.ts +4 -1
- package/dist/client/newsletter/content.js +43 -18
- package/dist/client/newsletter/messaging.d.ts +2 -0
- package/dist/client/newsletter/messaging.js +2 -1
- package/dist/client/newsletter/mex.d.ts +2 -2
- package/dist/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
- package/dist/client/persistence/ephemeral-setting.js +120 -0
- package/dist/client/persistence/group-history.d.ts +29 -0
- package/dist/client/persistence/group-history.js +149 -0
- package/dist/client/persistence/history-blob.d.ts +25 -0
- package/dist/client/persistence/history-blob.js +38 -0
- package/dist/client/persistence/history-sync.d.ts +2 -0
- package/dist/client/persistence/history-sync.js +31 -29
- package/dist/client/types.d.ts +150 -16
- package/dist/crypto/core/xeddsa.js +16 -0
- package/dist/crypto/curves/X25519.js +13 -0
- package/dist/crypto/nativeBackend.d.ts +20 -0
- package/dist/crypto/nativeBackend.js +95 -0
- package/dist/esm/abprops-spec.js +15 -0
- package/dist/esm/client/WaClient.js +60 -0
- package/dist/esm/client/WaClientFactory.js +51 -13
- package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
- package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
- package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
- package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
- package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
- package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
- package/dist/esm/client/events/dirty.js +12 -7
- package/dist/esm/client/events/offline.js +92 -0
- package/dist/esm/client/events/privacy.js +123 -0
- package/dist/esm/client/messaging/group-metadata.js +25 -9
- package/dist/esm/client/messaging/link-preview.js +29 -2
- package/dist/esm/client/newsletter/content.js +44 -19
- package/dist/esm/client/newsletter/messaging.js +2 -1
- package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
- package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
- package/dist/esm/client/persistence/group-history.js +145 -0
- package/dist/esm/client/persistence/history-blob.js +34 -0
- package/dist/esm/client/persistence/history-sync.js +24 -22
- package/dist/esm/crypto/core/xeddsa.js +17 -1
- package/dist/esm/crypto/curves/X25519.js +13 -0
- package/dist/esm/crypto/nativeBackend.js +93 -0
- package/dist/esm/index.js +3 -1
- package/dist/esm/message/context-info.js +39 -0
- package/dist/esm/message/encode/content.js +20 -4
- package/dist/esm/message/encode/media-payload.js +3 -1
- package/dist/esm/message/kinds/group-history.js +36 -0
- package/dist/esm/message/primitives/incoming.js +18 -6
- package/dist/esm/protocol/abprops.js +18 -162
- package/dist/esm/protocol/constants.js +4 -3
- package/dist/esm/protocol/defaults.js +6 -0
- package/dist/esm/protocol/media.js +1 -0
- package/dist/esm/protocol/message.js +8 -0
- package/dist/esm/protocol/notification.js +1 -0
- package/dist/esm/protocol/privacy.js +70 -4
- package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
- package/dist/esm/store/createStore.js +19 -2
- package/dist/esm/store/index.js +1 -0
- package/dist/esm/store/memory/chat-metadata.store.js +66 -0
- package/dist/esm/store/noop.store.js +8 -0
- package/dist/esm/transport/node/builders/privacy.js +63 -4
- package/dist/esm/util/coercion.js +12 -4
- package/dist/esm/util/index.js +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +15 -2
- package/dist/media/types.d.ts +1 -1
- package/dist/message/context-info.d.ts +10 -0
- package/dist/message/context-info.js +40 -0
- package/dist/message/encode/content.d.ts +7 -3
- package/dist/message/encode/content.js +20 -4
- package/dist/message/encode/media-payload.d.ts +5 -2
- package/dist/message/encode/media-payload.js +3 -1
- package/dist/message/kinds/group-history.d.ts +30 -0
- package/dist/message/kinds/group-history.js +40 -0
- package/dist/message/primitives/incoming.d.ts +5 -0
- package/dist/message/primitives/incoming.js +18 -6
- package/dist/protocol/abprops.d.ts +27 -140
- package/dist/protocol/abprops.js +18 -163
- package/dist/protocol/constants.d.ts +4 -3
- package/dist/protocol/constants.js +10 -3
- package/dist/protocol/defaults.d.ts +6 -0
- package/dist/protocol/defaults.js +6 -0
- package/dist/protocol/media.d.ts +1 -0
- package/dist/protocol/media.js +1 -0
- package/dist/protocol/message.d.ts +6 -0
- package/dist/protocol/message.js +9 -0
- package/dist/protocol/notification.d.ts +1 -0
- package/dist/protocol/notification.js +1 -0
- package/dist/protocol/privacy.d.ts +52 -0
- package/dist/protocol/privacy.js +71 -5
- package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
- package/dist/store/contracts/chat-metadata.store.js +2 -0
- package/dist/store/contracts/group-metadata.store.d.ts +7 -0
- package/dist/store/contracts/thread.store.d.ts +6 -0
- package/dist/store/createStore.d.ts +10 -2
- package/dist/store/createStore.js +18 -1
- package/dist/store/index.d.ts +3 -1
- package/dist/store/index.js +3 -1
- package/dist/store/memory/chat-metadata.store.d.ts +26 -0
- package/dist/store/memory/chat-metadata.store.js +70 -0
- package/dist/store/noop.store.d.ts +2 -0
- package/dist/store/noop.store.js +9 -1
- package/dist/store/types.d.ts +190 -39
- package/dist/transport/node/builders/privacy.d.ts +26 -2
- package/dist/transport/node/builders/privacy.js +63 -3
- package/dist/util/coercion.d.ts +6 -0
- package/dist/util/coercion.js +13 -4
- package/dist/util/index.d.ts +1 -1
- package/dist/util/index.js +2 -1
- package/package.json +3 -1
- package/spec/abprops/index.d.ts +4402 -0
- package/spec/abprops/index.js +4372 -0
|
@@ -55,7 +55,7 @@ import { SignalProtocol } from '../signal/session/SignalProtocol.js';
|
|
|
55
55
|
import { WaKeepAlive } from '../transport/keepalive/WaKeepAlive.js';
|
|
56
56
|
import { buildAckNode } from '../transport/node/builders/global.js';
|
|
57
57
|
import { buildPresenceNode } from '../transport/node/builders/presence.js';
|
|
58
|
-
import { getFirstNodeChild } from '../transport/node/helpers.js';
|
|
58
|
+
import { findNodeChild, getFirstNodeChild } from '../transport/node/helpers.js';
|
|
59
59
|
import { createUsyncSidGenerator } from '../transport/node/usync.js';
|
|
60
60
|
import { WaNodeOrchestrator } from '../transport/node/WaNodeOrchestrator.js';
|
|
61
61
|
import { WaNodeTransport } from '../transport/node/WaNodeTransport.js';
|
|
@@ -145,6 +145,7 @@ function createIncomingNodeRuntime(input) {
|
|
|
145
145
|
emitIncomingFailure: (event) => emitEvent('stream_failure', event),
|
|
146
146
|
emitIncomingErrorStanza: (event) => emitEvent('stanza_error', event),
|
|
147
147
|
emitIncomingNotification: (event) => emitEvent('debug_notification', event),
|
|
148
|
+
emitOfflineThreadMetadata: (event) => emitEvent('offline_thread_metadata', event),
|
|
148
149
|
emitMexNotification: (event) => emitEvent('mex_notification', event),
|
|
149
150
|
emitRegistrationCode: (event) => emitEvent('mobile_registration_code', event),
|
|
150
151
|
emitAccountTakeoverNotice: (event) => emitEvent('mobile_account_takeover_notice', event),
|
|
@@ -240,6 +241,15 @@ export function buildWaClientDependencies(input) {
|
|
|
240
241
|
allowPrivateHosts: linkPreviewOptions.allowPrivateHosts,
|
|
241
242
|
proxy: linkPreviewOptions.proxy ?? options.proxy?.linkPreview
|
|
242
243
|
});
|
|
244
|
+
const createLinkPreviewResolver = (surface) => (content) => resolveLinkPreview(content.text, content.linkPreview, {
|
|
245
|
+
logger,
|
|
246
|
+
mediaTransfer,
|
|
247
|
+
getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
|
|
248
|
+
fetcher: linkPreviewFetcher,
|
|
249
|
+
options: linkPreviewOptions,
|
|
250
|
+
serverClock,
|
|
251
|
+
surface
|
|
252
|
+
});
|
|
243
253
|
const mediaMessageBuildOptions = {
|
|
244
254
|
logger,
|
|
245
255
|
mediaTransfer,
|
|
@@ -257,14 +267,7 @@ export function buildWaClientDependencies(input) {
|
|
|
257
267
|
},
|
|
258
268
|
serverClock,
|
|
259
269
|
media: options.media,
|
|
260
|
-
linkPreviewResolver: (
|
|
261
|
-
logger,
|
|
262
|
-
mediaTransfer,
|
|
263
|
-
getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
|
|
264
|
-
fetcher: linkPreviewFetcher,
|
|
265
|
-
options: linkPreviewOptions,
|
|
266
|
-
serverClock
|
|
267
|
-
})
|
|
270
|
+
linkPreviewResolver: createLinkPreviewResolver()
|
|
268
271
|
};
|
|
269
272
|
const messageClient = new WaMessageClient({
|
|
270
273
|
logger,
|
|
@@ -367,12 +370,16 @@ export function buildWaClientDependencies(input) {
|
|
|
367
370
|
generateStanzaId: () => messageDispatch.generateOutgoingMessageId(),
|
|
368
371
|
mediaTransfer,
|
|
369
372
|
getMediaConn: () => getClientMediaConn(mediaMessageBuildOptions),
|
|
373
|
+
linkPreviewResolver: createLinkPreviewResolver('newsletter'),
|
|
370
374
|
getAbPropString: (name) => abPropsCoordinator.getConfigValue(name),
|
|
371
375
|
logger
|
|
372
376
|
});
|
|
373
377
|
const privacyCoordinator = createPrivacyCoordinator({
|
|
378
|
+
logger,
|
|
374
379
|
queryWithContext: runtime.queryWithContext,
|
|
375
|
-
resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid)
|
|
380
|
+
resolveUserJidPair: (userJid) => signalDeviceSync.resolveUserJidPair(userJid),
|
|
381
|
+
getSelfLid: () => getCurrentCredentials()?.meLid ?? null,
|
|
382
|
+
emitPrivacy: (event) => runtime.emitEvent('privacy', event)
|
|
376
383
|
});
|
|
377
384
|
const businessCoordinator = createBusinessCoordinator({
|
|
378
385
|
queryWithContext: runtime.queryWithContext,
|
|
@@ -411,7 +418,8 @@ export function buildWaClientDependencies(input) {
|
|
|
411
418
|
}
|
|
412
419
|
return {
|
|
413
420
|
participants: participantJids,
|
|
414
|
-
ephemeral: metadata.ephemeral
|
|
421
|
+
ephemeral: metadata.ephemeral,
|
|
422
|
+
ephemeralTrigger: metadata.ephemeralTrigger
|
|
415
423
|
};
|
|
416
424
|
},
|
|
417
425
|
logger
|
|
@@ -466,6 +474,8 @@ export function buildWaClientDependencies(input) {
|
|
|
466
474
|
sessionStore: sessionStore.session,
|
|
467
475
|
identityStore: sessionStore.identity,
|
|
468
476
|
deviceListStore: sessionStore.deviceList,
|
|
477
|
+
threadStore: sessionStore.threads,
|
|
478
|
+
chatMetadataStore: sessionStore.chatMetadata,
|
|
469
479
|
signalDeviceSync,
|
|
470
480
|
messageSecretStore: sessionStore.messageSecret,
|
|
471
481
|
persistAllMessageSecrets: options.addons?.persistAllSecrets === true,
|
|
@@ -508,7 +518,9 @@ export function buildWaClientDependencies(input) {
|
|
|
508
518
|
trustedContactToken,
|
|
509
519
|
emitAddon: (event) => runtime.emitEvent('message_addon', event),
|
|
510
520
|
mexSocket: { query: runtime.query },
|
|
511
|
-
peerDataOperation
|
|
521
|
+
peerDataOperation,
|
|
522
|
+
isGroupHistorySendEnabled: () => abPropsCoordinator.getConfigValue('group_history_send'),
|
|
523
|
+
getAbPropNumber: (name) => abPropsCoordinator.getConfigValue(name)
|
|
512
524
|
});
|
|
513
525
|
const retryCoordinator = new WaRetryCoordinator({
|
|
514
526
|
logger,
|
|
@@ -533,7 +545,8 @@ export function buildWaClientDependencies(input) {
|
|
|
533
545
|
.handleIncomingMessageEvent(event)
|
|
534
546
|
.catch((err) => runtime.handleError(toError(err)));
|
|
535
547
|
},
|
|
536
|
-
isMobilePrimary
|
|
548
|
+
isMobilePrimary,
|
|
549
|
+
getAbPropNumber: (name) => abPropsCoordinator.getConfigValue(name)
|
|
537
550
|
});
|
|
538
551
|
const botCoordinator = createBotCoordinator({
|
|
539
552
|
logger,
|
|
@@ -645,6 +658,7 @@ export function buildWaClientDependencies(input) {
|
|
|
645
658
|
const disconnectWithClientSideEffects = async (reason, isLogout, code) => {
|
|
646
659
|
abPropsCoordinator.reset();
|
|
647
660
|
offlineResume.reset();
|
|
661
|
+
privacyCoordinator.stopAccountSyncRefresh();
|
|
648
662
|
await connectionManager?.disconnect();
|
|
649
663
|
runtime.emitEvent('connection', {
|
|
650
664
|
status: 'close',
|
|
@@ -691,6 +705,7 @@ export function buildWaClientDependencies(input) {
|
|
|
691
705
|
signalProtocol,
|
|
692
706
|
senderKeyManager,
|
|
693
707
|
onDecryptFailure: (context, error) => retryCoordinator.onDecryptFailure(context, error),
|
|
708
|
+
requestPlaceholderResend: (context) => retryCoordinator.onUnavailableMessage(context),
|
|
694
709
|
emitIncomingMessage: (event) => {
|
|
695
710
|
void runtime
|
|
696
711
|
.handleIncomingMessageEvent(event)
|
|
@@ -706,6 +721,10 @@ export function buildWaClientDependencies(input) {
|
|
|
706
721
|
getCurrentCredentials,
|
|
707
722
|
syncAppState: runtime.syncAppState,
|
|
708
723
|
generateUsyncSid,
|
|
724
|
+
syncAccountPrivacy: async () => {
|
|
725
|
+
await privacyCoordinator.refreshFromAccountSync();
|
|
726
|
+
},
|
|
727
|
+
emitBlocklist: (blocklist) => runtime.emitEvent('blocklist', blocklist),
|
|
709
728
|
newsletterListSubscribed: () => newsletterCoordinator.listSubscribed()
|
|
710
729
|
}, dirtyBits);
|
|
711
730
|
const offlineResume = new WaOfflineResumeCoordinator({
|
|
@@ -887,6 +906,25 @@ export function buildWaClientDependencies(input) {
|
|
|
887
906
|
return false;
|
|
888
907
|
}
|
|
889
908
|
});
|
|
909
|
+
incomingNode.registerIncomingHandler({
|
|
910
|
+
tag: WA_NODE_TAGS.NOTIFICATION,
|
|
911
|
+
subtype: WA_NOTIFICATION_TYPES.ACCOUNT_SYNC,
|
|
912
|
+
prepend: true,
|
|
913
|
+
/**
|
|
914
|
+
* The stanza carries the changed `<category>` inline, but it is a
|
|
915
|
+
* trigger only: the payload is a partial view, so the whole set is
|
|
916
|
+
* refetched instead. Observer only - returning false keeps the
|
|
917
|
+
* generic notification handler responsible for the ack and the
|
|
918
|
+
* `debug_notification` emit, leaving wire behavior unchanged.
|
|
919
|
+
*/
|
|
920
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
921
|
+
handler: async (node) => {
|
|
922
|
+
if (findNodeChild(node, WA_NODE_TAGS.PRIVACY)) {
|
|
923
|
+
privacyCoordinator.scheduleAccountSyncRefresh();
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
});
|
|
890
928
|
incomingNode.registerIncomingHandler({
|
|
891
929
|
tag: WA_NODE_TAGS.NOTIFICATION,
|
|
892
930
|
subtype: WA_NOTIFICATION_TYPES.DEVICES,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { resolveAbPropNameByCode, WA_ABPROPS } from '../../abprops-spec.js';
|
|
1
2
|
import { parseAbPropsIqResult } from '../events/abprops.js';
|
|
2
|
-
import {
|
|
3
|
+
import { WA_ABPROPS_REFRESH_BOUNDS } from '../../protocol/abprops.js';
|
|
3
4
|
import { WA_DEFAULTS } from '../../protocol/constants.js';
|
|
4
5
|
import { buildGetAbPropsIq } from '../../transport/node/builders/abprops.js';
|
|
5
|
-
import {
|
|
6
|
+
import { toError } from '../../util/primitives.js';
|
|
7
|
+
// The generated table types every entry with literal `code` / `defaultValue`
|
|
8
|
+
// types. Reading through a widened view keeps a lookup by a non-literal name
|
|
9
|
+
// from expanding into a 2000-member union at every call site.
|
|
10
|
+
const AB_PROPS = WA_ABPROPS;
|
|
6
11
|
export class WaAbPropsCoordinator {
|
|
7
12
|
constructor(options) {
|
|
8
13
|
this.logger = options.logger;
|
|
@@ -18,7 +23,7 @@ export class WaAbPropsCoordinator {
|
|
|
18
23
|
if (cached !== undefined) {
|
|
19
24
|
return cached;
|
|
20
25
|
}
|
|
21
|
-
return
|
|
26
|
+
return AB_PROPS[name].defaultValue;
|
|
22
27
|
}
|
|
23
28
|
sync() {
|
|
24
29
|
if (this.syncPromise) {
|
|
@@ -105,7 +110,7 @@ export class WaAbPropsCoordinator {
|
|
|
105
110
|
if (!name) {
|
|
106
111
|
continue;
|
|
107
112
|
}
|
|
108
|
-
const config =
|
|
113
|
+
const config = AB_PROPS[name];
|
|
109
114
|
const parsed = parseConfigValue(entry.configValue, config.type, config.defaultValue);
|
|
110
115
|
this.cache.set(name, parsed);
|
|
111
116
|
applied += 1;
|
|
@@ -119,6 +124,8 @@ export class WaAbPropsCoordinator {
|
|
|
119
124
|
});
|
|
120
125
|
}
|
|
121
126
|
}
|
|
127
|
+
// parseInt, not a digits-only parse: negative values are in band, several props
|
|
128
|
+
// ship -1 as their default. Divergence: WA Web caches NaN, we keep the default.
|
|
122
129
|
function parseConfigValue(value, type, defaultValue) {
|
|
123
130
|
if (value === null) {
|
|
124
131
|
return defaultValue;
|
|
@@ -127,7 +134,12 @@ function parseConfigValue(value, type, defaultValue) {
|
|
|
127
134
|
return value === '1' || value === 'true' || value === 'True';
|
|
128
135
|
}
|
|
129
136
|
if (type === 'int') {
|
|
130
|
-
|
|
137
|
+
const parsed = Number.parseInt(value, 10);
|
|
138
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
139
|
+
}
|
|
140
|
+
if (type === 'float') {
|
|
141
|
+
const parsed = Number.parseFloat(value);
|
|
142
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
131
143
|
}
|
|
132
144
|
return value;
|
|
133
145
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseChatstateNode } from '../events/chatstate.js';
|
|
2
2
|
import { buildInboundAck, createIncomingBaseEvent, createIncomingBusinessNotificationHandler, createIncomingCallHandler, createIncomingFailureHandler, createIncomingGroupNotificationHandler, createIncomingNotificationHandler, createIncomingPictureNotificationHandler, createIncomingReceiptHandler, createIncomingRegistrationNotificationHandler, createInfoBulletinNotificationEvent, createUnhandledIncomingNodeEvent, sendSafeAck } from '../events/incoming.js';
|
|
3
|
+
import { parseOfflineThreadMetadata } from '../events/offline.js';
|
|
3
4
|
import { parsePresenceNode } from '../events/presence.js';
|
|
4
5
|
import { WA_IQ_TYPES, WA_MESSAGE_TAGS, WA_MESSAGE_TYPES, WA_NODE_TAGS, WA_NOTIFICATION_TYPES, WA_SIGNALING } from '../../protocol/constants.js';
|
|
5
6
|
import { decodeNodeContentBase64OrBytes, findNodeChild, getNodeChildren, getNodeChildrenByTag } from '../../transport/node/helpers.js';
|
|
@@ -378,6 +379,9 @@ export class WaIncomingNodeCoordinator {
|
|
|
378
379
|
if (child.tag === 'offline') {
|
|
379
380
|
this.offlineResume.handleOfflineComplete(parseOptionalInt(child.attrs.count) ?? 0);
|
|
380
381
|
}
|
|
382
|
+
if (child.tag === 'thread_metadata') {
|
|
383
|
+
this.runtime.emitOfflineThreadMetadata(parseOfflineThreadMetadata(child));
|
|
384
|
+
}
|
|
381
385
|
handled = true;
|
|
382
386
|
}
|
|
383
387
|
}
|
|
@@ -6,12 +6,26 @@ import { uploadMedia } from '../messaging/messages.js';
|
|
|
6
6
|
import { MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
|
|
7
7
|
import { buildAddonAdditionalData, decodeAddonPlaintext, decryptAddonPayload, identifyEncryptedAddon, resolveParentMessageSecret, resolvePollOptionNames, shouldUseAddonAdditionalData } from '../../message/crypto/addon-crypto.js';
|
|
8
8
|
import { unwrapMessage } from '../../message/encode/content.js';
|
|
9
|
+
import { encodeGroupHistoryBundle } from '../../message/kinds/group-history.js';
|
|
9
10
|
import { proto } from '../../proto.js';
|
|
10
|
-
import { applyDeviceToJid, normalizeRecipientJid } from '../../protocol/jid.js';
|
|
11
|
+
import { applyDeviceToJid, isGroupJid, normalizeRecipientJid } from '../../protocol/jid.js';
|
|
11
12
|
import { runMexQuery } from '../../transport/node/mex/client.js';
|
|
12
13
|
import { readAllBytes } from '../../util/bytes.js';
|
|
13
|
-
import { tryAsNumber, tryAsString } from '../../util/coercion.js';
|
|
14
|
-
import { toError } from '../../util/primitives.js';
|
|
14
|
+
import { resolveOptionalPositive, resolvePositive, tryAsNumber, tryAsString } from '../../util/coercion.js';
|
|
15
|
+
import { longToNumber, toError } from '../../util/primitives.js';
|
|
16
|
+
/** MIME type the group-history bundle is uploaded and advertised under. */
|
|
17
|
+
const GROUP_HISTORY_BUNDLE_MIMETYPE = 'application/protobuf';
|
|
18
|
+
/** Oldest `messageTimestamp` across `messages`, in seconds; `undefined` when none carries one. */
|
|
19
|
+
function oldestTimestampSeconds(messages) {
|
|
20
|
+
let oldest;
|
|
21
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
22
|
+
const timestamp = longToNumber(messages[index].messageTimestamp);
|
|
23
|
+
if (timestamp > 0 && (oldest === undefined || timestamp < oldest)) {
|
|
24
|
+
oldest = timestamp;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return oldest;
|
|
28
|
+
}
|
|
15
29
|
function parseReachoutTimelockMexResponse(data) {
|
|
16
30
|
const root = data?.xwa2_fetch_account_reachout_timelock;
|
|
17
31
|
return {
|
|
@@ -70,6 +84,8 @@ export class WaMessageCoordinator {
|
|
|
70
84
|
this.emitAddon = deps.emitAddon;
|
|
71
85
|
this.mexSocket = deps.mexSocket;
|
|
72
86
|
this.peerDataOperation = deps.peerDataOperation;
|
|
87
|
+
this.isGroupHistorySendEnabled = deps.isGroupHistorySendEnabled;
|
|
88
|
+
this.getAbPropNumber = deps.getAbPropNumber;
|
|
73
89
|
}
|
|
74
90
|
/**
|
|
75
91
|
* Asks the server to backfill older messages for `chatJid` beyond what
|
|
@@ -86,13 +102,7 @@ export class WaMessageCoordinator {
|
|
|
86
102
|
*/
|
|
87
103
|
async requestHistorySync(input) {
|
|
88
104
|
const chatJid = normalizeRecipientJid(input.chatJid);
|
|
89
|
-
|
|
90
|
-
if (!Number.isFinite(input.count) ||
|
|
91
|
-
!Number.isSafeInteger(input.count) ||
|
|
92
|
-
input.count <= 0) {
|
|
93
|
-
throw new Error(`invalid count: ${input.count}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
105
|
+
const onDemandMsgCount = resolveOptionalPositive(input.count, 'count');
|
|
96
106
|
if (input.oldestMsgTimestampMs !== undefined) {
|
|
97
107
|
if (!Number.isFinite(input.oldestMsgTimestampMs) ||
|
|
98
108
|
!Number.isSafeInteger(input.oldestMsgTimestampMs) ||
|
|
@@ -110,10 +120,155 @@ export class WaMessageCoordinator {
|
|
|
110
120
|
...(input.oldestMsgTimestampMs === undefined
|
|
111
121
|
? {}
|
|
112
122
|
: { oldestMsgTimestampMs: input.oldestMsgTimestampMs }),
|
|
113
|
-
...(
|
|
123
|
+
...(onDemandMsgCount === undefined ? {} : { onDemandMsgCount })
|
|
114
124
|
};
|
|
115
125
|
return this.peerDataOperation.send(proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND, { historySyncOnDemandRequest });
|
|
116
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Shares recent group history with members who joined after the fact - the
|
|
129
|
+
* counterpart of the `group_history_bundle` event on the receiving side.
|
|
130
|
+
*
|
|
131
|
+
* Sends two messages: the bundle itself, encrypted per device and fanned
|
|
132
|
+
* out **only** to `toJids` (never to the whole group), followed by a hidden
|
|
133
|
+
* notice addressed to everyone so other clients can render the "history was
|
|
134
|
+
* shared" marker.
|
|
135
|
+
*
|
|
136
|
+
* WhatsApp gates the sender side per account through the `group_history_send`
|
|
137
|
+
* AB-prop and rejects the stanza with SMAX_INVALID when it is off, so this
|
|
138
|
+
* throws up front rather than spending an upload. Admin-only groups
|
|
139
|
+
* (`memberShareGroupHistoryMode: 'admin_share'`) reject a share from a
|
|
140
|
+
* regular member server-side - read `client.group.queryGroupMetadata()`
|
|
141
|
+
* first if you want to check that too.
|
|
142
|
+
*
|
|
143
|
+
* @throws when the account is not allowed to share group history, when
|
|
144
|
+
* `groupJid` is not a group, when any of `toJids` is not a current member
|
|
145
|
+
* (or is this account), or when there is nothing to bundle.
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* await client.message.shareGroupHistory('12036@g.us', {
|
|
149
|
+
* toJids: ['5511999999999@s.whatsapp.net'],
|
|
150
|
+
* count: 50
|
|
151
|
+
* })
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
async shareGroupHistory(groupJid, input) {
|
|
155
|
+
const normalizedGroupJid = normalizeRecipientJid(groupJid);
|
|
156
|
+
if (!isGroupJid(normalizedGroupJid)) {
|
|
157
|
+
throw new Error(`shareGroupHistory requires a group jid: ${normalizedGroupJid}`);
|
|
158
|
+
}
|
|
159
|
+
if (input.toJids.length === 0) {
|
|
160
|
+
throw new Error('shareGroupHistory requires at least one recipient');
|
|
161
|
+
}
|
|
162
|
+
const messageLimit = resolvePositive(input.count, this.getAbPropNumber('group_history_message_count_limit'), 'count');
|
|
163
|
+
if (!this.isGroupHistorySendEnabled()) {
|
|
164
|
+
throw new Error('shareGroupHistory is disabled for this account (group_history_send is off)');
|
|
165
|
+
}
|
|
166
|
+
const audience = await this.messageDispatch.resolveGroupHistoryAudience(normalizedGroupJid, input.toJids);
|
|
167
|
+
if (audience.requestedSelf) {
|
|
168
|
+
throw new Error('shareGroupHistory cannot share history with this account itself');
|
|
169
|
+
}
|
|
170
|
+
if (audience.unknownJids.length > 0) {
|
|
171
|
+
throw new Error(`shareGroupHistory recipients are not members of the group (addressing mode: ${audience.addressingMode}): ${audience.unknownJids.join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
if (audience.historyReceivers.length === 0) {
|
|
174
|
+
throw new Error('shareGroupHistory resolved no recipients');
|
|
175
|
+
}
|
|
176
|
+
const messages = input.messages ??
|
|
177
|
+
(await this.loadGroupHistoryMessages(normalizedGroupJid, messageLimit, input.sinceMs));
|
|
178
|
+
if (messages.length === 0) {
|
|
179
|
+
throw new Error('shareGroupHistory found no messages to share');
|
|
180
|
+
}
|
|
181
|
+
const { compressed } = await encodeGroupHistoryBundle(messages, input.outOfWindowPinnedMessages);
|
|
182
|
+
const upload = await uploadMedia(this.mediaUploadOptions, {
|
|
183
|
+
source: compressed,
|
|
184
|
+
cryptoType: 'group-history',
|
|
185
|
+
uploadPath: MEDIA_UPLOAD_PATHS['group-history'],
|
|
186
|
+
contentType: GROUP_HISTORY_BUNDLE_MIMETYPE,
|
|
187
|
+
sidecar: false,
|
|
188
|
+
logLabel: 'group history bundle upload'
|
|
189
|
+
});
|
|
190
|
+
const oldestInWindow = oldestTimestampSeconds(messages);
|
|
191
|
+
const oldestPin = oldestTimestampSeconds(input.outOfWindowPinnedMessages ?? []);
|
|
192
|
+
const oldestInBundle = oldestPin === undefined
|
|
193
|
+
? oldestInWindow
|
|
194
|
+
: oldestInWindow === undefined
|
|
195
|
+
? oldestPin
|
|
196
|
+
: Math.min(oldestInWindow, oldestPin);
|
|
197
|
+
const metadata = {
|
|
198
|
+
historyReceivers: audience.historyReceivers,
|
|
199
|
+
nonHistoryReceivers: audience.nonHistoryReceivers,
|
|
200
|
+
messageCount: messages.length,
|
|
201
|
+
oldestMessageTimestampInWindow: oldestInWindow,
|
|
202
|
+
oldestMessageTimestampInBundle: oldestInBundle
|
|
203
|
+
};
|
|
204
|
+
const bundleResult = await this.send(normalizedGroupJid, {
|
|
205
|
+
messageHistoryBundle: {
|
|
206
|
+
mimetype: GROUP_HISTORY_BUNDLE_MIMETYPE,
|
|
207
|
+
fileSha256: upload.fileSha256,
|
|
208
|
+
fileEncSha256: upload.fileEncSha256,
|
|
209
|
+
mediaKey: upload.mediaKey,
|
|
210
|
+
directPath: upload.directPath,
|
|
211
|
+
mediaKeyTimestamp: this.mediaUploadOptions.serverClock.nowSeconds(),
|
|
212
|
+
messageHistoryMetadata: metadata
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
let noticeMessageId;
|
|
216
|
+
try {
|
|
217
|
+
noticeMessageId = (await this.send(normalizedGroupJid, {
|
|
218
|
+
messageHistoryNotice: { messageHistoryMetadata: metadata }
|
|
219
|
+
})).id;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
this.logger.warn('group history notice failed after the bundle was delivered', {
|
|
223
|
+
groupJid: normalizedGroupJid,
|
|
224
|
+
id: bundleResult.id,
|
|
225
|
+
receiverCount: audience.historyReceivers.length,
|
|
226
|
+
message: toError(error).message
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
this.logger.debug('shared group history', {
|
|
230
|
+
groupJid: normalizedGroupJid,
|
|
231
|
+
id: bundleResult.id,
|
|
232
|
+
messagesCount: messages.length,
|
|
233
|
+
receiverCount: audience.historyReceivers.length
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
bundleMessageId: bundleResult.id,
|
|
237
|
+
noticeMessageId,
|
|
238
|
+
messagesCount: messages.length,
|
|
239
|
+
historyReceivers: audience.historyReceivers,
|
|
240
|
+
nonHistoryReceivers: audience.nonHistoryReceivers
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Reads the most recent messages of a group back out of the mailbox store
|
|
245
|
+
* and rebuilds the `WebMessageInfo` shape a bundle carries. Yields nothing
|
|
246
|
+
* when the `messages` store domain is `'none'`.
|
|
247
|
+
*/
|
|
248
|
+
async loadGroupHistoryMessages(groupJid, limit, sinceMs) {
|
|
249
|
+
const records = await this.messageStore.listByThread(groupJid, limit);
|
|
250
|
+
const messages = [];
|
|
251
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
252
|
+
const record = records[index];
|
|
253
|
+
if (!record.messageBytes) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (sinceMs !== undefined && (record.timestampMs ?? 0) < sinceMs) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
messages[messages.length] = {
|
|
260
|
+
key: {
|
|
261
|
+
id: record.id,
|
|
262
|
+
remoteJid: groupJid,
|
|
263
|
+
fromMe: record.fromMe,
|
|
264
|
+
participant: record.participantJid ?? record.senderJid
|
|
265
|
+
},
|
|
266
|
+
message: proto.Message.decode(record.messageBytes),
|
|
267
|
+
messageTimestamp: Math.floor((record.timestampMs ?? 0) / 1000)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return messages;
|
|
271
|
+
}
|
|
117
272
|
/**
|
|
118
273
|
* Fetches the server-side "reachout" timelock that throttles cold outreach
|
|
119
274
|
* to non-contacts, returning the active window when enforcement is on.
|