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
|
@@ -11,7 +11,9 @@ import { WaRetryReplayService } from '../../retry/replay.js';
|
|
|
11
11
|
import { generatePreKeyPair } from '../../signal/registration/keygen.js';
|
|
12
12
|
import { buildAckNode } from '../../transport/node/builders/global.js';
|
|
13
13
|
import { buildRetryReceiptNode } from '../../transport/node/builders/retry.js';
|
|
14
|
+
import { findNodeChild } from '../../transport/node/helpers.js';
|
|
14
15
|
import { uint8Equal } from '../../util/bytes.js';
|
|
16
|
+
import { tryAsNumber } from '../../util/coercion.js';
|
|
15
17
|
import { setBoundedMapEntry } from '../../util/collections.js';
|
|
16
18
|
import { toError } from '../../util/primitives.js';
|
|
17
19
|
const RETRY_CLEANUP_INTERVAL_MS = 30000;
|
|
@@ -23,8 +25,10 @@ const DECRYPT_FAILURE_QUEUE_MAX_CONCURRENCY = 8;
|
|
|
23
25
|
const PLACEHOLDER_RESEND_RETRY_THRESHOLD = 3;
|
|
24
26
|
const PLACEHOLDER_RESEND_BATCH_SIZE = 32;
|
|
25
27
|
const PLACEHOLDER_RESEND_DEBOUNCE_MS = 200;
|
|
26
|
-
const
|
|
28
|
+
const PLACEHOLDER_RESEND_FALLBACK_MAX_AGE_DAYS = 14;
|
|
27
29
|
const PLACEHOLDER_RESEND_IN_FLIGHT_MAX = 256;
|
|
30
|
+
const PLACEHOLDER_RESEND_TIMEOUT_MS = 120000;
|
|
31
|
+
const DAY_SECONDS = 24 * 60 * 60;
|
|
28
32
|
const PLACEHOLDER_RESEND_SKIP_SUBTYPES = new Set([
|
|
29
33
|
'bot_unavailable_fanout',
|
|
30
34
|
'hosted_unavailable_fanout',
|
|
@@ -47,6 +51,30 @@ function getRemoteRetryReasonLogFields(reason) {
|
|
|
47
51
|
remoteRetryReasonName: getRetryReasonName(reason) ?? 'missing_in_retry_receipt'
|
|
48
52
|
};
|
|
49
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns the placeholder subtype the primary never resends (bot fanout, hosted
|
|
56
|
+
* account, consumed view-once), or `null` when the message is recoverable.
|
|
57
|
+
*/
|
|
58
|
+
function resolvePlaceholderResendSkipSubtype(node) {
|
|
59
|
+
const subtype = node.attrs.subtype;
|
|
60
|
+
if (typeof subtype === 'string' && PLACEHOLDER_RESEND_SKIP_SUBTYPES.has(subtype)) {
|
|
61
|
+
return subtype;
|
|
62
|
+
}
|
|
63
|
+
const unavailable = findNodeChild(node, 'unavailable');
|
|
64
|
+
if (!unavailable) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (findNodeChild(node, 'bot')) {
|
|
68
|
+
return 'bot_unavailable_fanout';
|
|
69
|
+
}
|
|
70
|
+
if (unavailable.attrs.hosted === 'true') {
|
|
71
|
+
return 'hosted_unavailable_fanout';
|
|
72
|
+
}
|
|
73
|
+
if (unavailable.attrs.type === 'view_once') {
|
|
74
|
+
return 'view_once_unavailable_fanout';
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
50
78
|
export class WaRetryCoordinator {
|
|
51
79
|
constructor(options) {
|
|
52
80
|
this.nextRetryCleanupAtMs = 0;
|
|
@@ -765,6 +793,42 @@ export class WaRetryCoordinator {
|
|
|
765
793
|
const meJid = credentials?.meJid;
|
|
766
794
|
return !!meJid && parseSignalAddressFromJid(meJid).user === senderUser;
|
|
767
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Queues a `PLACEHOLDER_MESSAGE_RESEND` peer request for a message the server
|
|
798
|
+
* delivered as `<unavailable/>` (no `<enc>` to decrypt, so no retry receipt).
|
|
799
|
+
* Returns `false` when the placeholder is unrecoverable, too old, or this
|
|
800
|
+
* session is the primary. The payload surfaces later as a `message` event.
|
|
801
|
+
*/
|
|
802
|
+
onUnavailableMessage(context) {
|
|
803
|
+
return this.enqueuePlaceholderResend(context);
|
|
804
|
+
}
|
|
805
|
+
getPlaceholderResendMaxAgeSeconds() {
|
|
806
|
+
const days = tryAsNumber(this.deps.getAbPropNumber?.('placeholder_message_resend_maximum_days_limit'));
|
|
807
|
+
return ((days !== null && days > 0 ? days : PLACEHOLDER_RESEND_FALLBACK_MAX_AGE_DAYS) *
|
|
808
|
+
DAY_SECONDS);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Builds the message key the primary indexes the message under: the chat is
|
|
812
|
+
* the `from`, or the `recipient` for a self-sent 1:1, always without the
|
|
813
|
+
* `:device` segment. The participant rides along only for an incoming
|
|
814
|
+
* group/broadcast message. Asking with the raw device-addressed `from`
|
|
815
|
+
* matches nothing and the resend comes back empty.
|
|
816
|
+
*/
|
|
817
|
+
buildPlaceholderResendKey(context) {
|
|
818
|
+
const fromMe = this.isSenderFromOwnAccount(context);
|
|
819
|
+
const isGroupOrBroadcast = isGroupOrBroadcastJid(context.from);
|
|
820
|
+
const chatJid = fromMe && !isGroupOrBroadcast
|
|
821
|
+
? (context.messageNode.attrs.recipient ?? context.from)
|
|
822
|
+
: context.from;
|
|
823
|
+
return {
|
|
824
|
+
remoteJid: toUserJid(chatJid),
|
|
825
|
+
id: context.stanzaId,
|
|
826
|
+
fromMe,
|
|
827
|
+
...(!fromMe && isGroupOrBroadcast && context.participant
|
|
828
|
+
? { participant: toUserJid(context.participant) }
|
|
829
|
+
: {})
|
|
830
|
+
};
|
|
831
|
+
}
|
|
768
832
|
enqueuePlaceholderResend(context) {
|
|
769
833
|
if (!this.deps.peerDataOperation || !this.deps.emitIncomingMessage) {
|
|
770
834
|
return false;
|
|
@@ -772,14 +836,18 @@ export class WaRetryCoordinator {
|
|
|
772
836
|
if (this.deps.isMobilePrimary?.()) {
|
|
773
837
|
return false;
|
|
774
838
|
}
|
|
775
|
-
const
|
|
776
|
-
if (
|
|
839
|
+
const skipSubtype = resolvePlaceholderResendSkipSubtype(context.messageNode);
|
|
840
|
+
if (skipSubtype !== null) {
|
|
841
|
+
this.deps.logger.trace('placeholder resend skipped: unrecoverable placeholder', {
|
|
842
|
+
id: context.stanzaId,
|
|
843
|
+
subtype: skipSubtype
|
|
844
|
+
});
|
|
777
845
|
return false;
|
|
778
846
|
}
|
|
779
847
|
const timestampSeconds = context.t ? Number(context.t) : Date.now() / 1000;
|
|
780
848
|
if (Number.isFinite(timestampSeconds)) {
|
|
781
849
|
const ageSeconds = Date.now() / 1000 - timestampSeconds;
|
|
782
|
-
if (ageSeconds >
|
|
850
|
+
if (ageSeconds > this.getPlaceholderResendMaxAgeSeconds()) {
|
|
783
851
|
return false;
|
|
784
852
|
}
|
|
785
853
|
}
|
|
@@ -794,12 +862,7 @@ export class WaRetryCoordinator {
|
|
|
794
862
|
return false;
|
|
795
863
|
}
|
|
796
864
|
this.placeholderInFlight.add(context.stanzaId);
|
|
797
|
-
this.placeholderQueue.push(
|
|
798
|
-
remoteJid: context.from,
|
|
799
|
-
id: context.stanzaId,
|
|
800
|
-
fromMe: this.isSenderFromOwnAccount(context),
|
|
801
|
-
participant: context.participant
|
|
802
|
-
});
|
|
865
|
+
this.placeholderQueue.push(this.buildPlaceholderResendKey(context));
|
|
803
866
|
if (this.placeholderTimer === null) {
|
|
804
867
|
this.placeholderTimer = setTimeout(() => {
|
|
805
868
|
this.placeholderTimer = null;
|
|
@@ -828,7 +891,7 @@ export class WaRetryCoordinator {
|
|
|
828
891
|
participant: item.participant
|
|
829
892
|
}
|
|
830
893
|
}))
|
|
831
|
-
});
|
|
894
|
+
}, { timeoutMs: PLACEHOLDER_RESEND_TIMEOUT_MS });
|
|
832
895
|
const meJid = this.deps.getCurrentCredentials()?.meJid;
|
|
833
896
|
for (const result of results) {
|
|
834
897
|
const bytes = result.placeholderMessageResendResponse?.webMessageInfoBytes;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { parseBlocklist } from '../events/privacy.js';
|
|
1
2
|
import { WA_ACCOUNT_SYNC_PROTOCOLS, WA_DEFAULTS, WA_DIRTY_PROTOCOLS, WA_DIRTY_TYPES, WA_IQ_TYPES, WA_NODE_TAGS, WA_SUPPORTED_DIRTY_TYPES } from '../../protocol/constants.js';
|
|
2
3
|
import { toUserJid } from '../../protocol/jid.js';
|
|
3
4
|
import { buildAccountBlocklistSyncIq, buildAccountDevicesSyncIq, buildAccountPictureSyncIq, buildClearDirtyBitsIq, buildListParticipatingGroupsIq, buildNewsletterMetadataSyncIq } from '../../transport/node/builders/account-sync.js';
|
|
4
|
-
import { buildGetPrivacySettingsIq } from '../../transport/node/builders/privacy.js';
|
|
5
5
|
import { parseUsyncResultEnvelope } from '../../transport/node/builders/usync.js';
|
|
6
6
|
import { getNodeChildrenTags } from '../../transport/node/helpers.js';
|
|
7
7
|
import { assertIqResult, parseIqError } from '../../transport/node/query.js';
|
|
@@ -219,19 +219,23 @@ async function syncAccountPictureDirtyBit(runtime) {
|
|
|
219
219
|
}
|
|
220
220
|
throw new Error(`account_sync.picture iq failed (${iqError.code}: ${iqError.text})`);
|
|
221
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Catch-up path for a session that was offline while another device changed a
|
|
224
|
+
* setting. Runs the same refresh the live `account_sync` notification
|
|
225
|
+
* triggers: the whole category set plus the disallowed lists.
|
|
226
|
+
*/
|
|
222
227
|
async function syncAccountPrivacyDirtyBit(runtime) {
|
|
223
|
-
await
|
|
224
|
-
|
|
225
|
-
node: buildGetPrivacySettingsIq(),
|
|
226
|
-
logMessage: 'account_sync privacy synchronized'
|
|
227
|
-
});
|
|
228
|
+
await runtime.syncAccountPrivacy();
|
|
229
|
+
runtime.logger.debug('account_sync privacy synchronized');
|
|
228
230
|
}
|
|
231
|
+
/** The reply carries the full blocklist, not a delta. */
|
|
229
232
|
async function syncAccountBlocklistDirtyBit(runtime) {
|
|
230
|
-
await runSyncQuery(runtime, {
|
|
233
|
+
const response = await runSyncQuery(runtime, {
|
|
231
234
|
queryContext: 'account_sync.blocklist',
|
|
232
235
|
node: buildAccountBlocklistSyncIq(),
|
|
233
236
|
logMessage: 'account_sync blocklist synchronized'
|
|
234
237
|
});
|
|
238
|
+
runtime.emitBlocklist(parseBlocklist(response));
|
|
235
239
|
}
|
|
236
240
|
async function syncGroupsDirtyBit(runtime) {
|
|
237
241
|
await runSyncQuery(runtime, {
|
|
@@ -271,6 +275,7 @@ async function runSyncQuery(runtime, args) {
|
|
|
271
275
|
assertIqResult(response, args.assertContext ?? args.queryContext);
|
|
272
276
|
logUsyncProtocolErrors(parseUsyncResultEnvelope(response), runtime.logger, args.queryContext);
|
|
273
277
|
runtime.logger.debug(args.logMessage, args.contextData);
|
|
278
|
+
return response;
|
|
274
279
|
}
|
|
275
280
|
async function clearDirtyBits(runtime, dirtyBits) {
|
|
276
281
|
try {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { findNodeChild, getNodeChildrenByTag } from '../../transport/node/helpers.js';
|
|
2
|
+
import { parseOptionalInt } from '../../util/primitives.js';
|
|
3
|
+
const OFFLINE_THREAD_TAGS = Object.freeze({
|
|
4
|
+
ITEM: 'item',
|
|
5
|
+
WATERMARK: 'watermark',
|
|
6
|
+
STATUS_MSGS: 'status_msgs',
|
|
7
|
+
NOTIFICATIONS: 'notifications'
|
|
8
|
+
});
|
|
9
|
+
function parseThreadPreviews(node) {
|
|
10
|
+
const items = getNodeChildrenByTag(node, OFFLINE_THREAD_TAGS.ITEM);
|
|
11
|
+
const previews = [];
|
|
12
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
13
|
+
const item = items[index];
|
|
14
|
+
const jid = item.attrs.from;
|
|
15
|
+
const timestampSeconds = parseOptionalInt(item.attrs.t);
|
|
16
|
+
if (!jid || timestampSeconds === undefined || timestampSeconds < 0) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
previews.push({ jid, timestampSeconds });
|
|
20
|
+
}
|
|
21
|
+
return previews;
|
|
22
|
+
}
|
|
23
|
+
/** Keeps the highest `sts` per jid; non-positive means "no watermark", not "read at epoch". */
|
|
24
|
+
function parseReadWatermarks(node) {
|
|
25
|
+
const watermarkNode = findNodeChild(node, OFFLINE_THREAD_TAGS.WATERMARK);
|
|
26
|
+
if (!watermarkNode) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const highestByJid = new Map();
|
|
30
|
+
const items = getNodeChildrenByTag(watermarkNode, OFFLINE_THREAD_TAGS.ITEM);
|
|
31
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
32
|
+
const item = items[index];
|
|
33
|
+
const jid = item.attrs.from;
|
|
34
|
+
const readTimestampSeconds = parseOptionalInt(item.attrs.sts);
|
|
35
|
+
if (!jid || readTimestampSeconds === undefined || readTimestampSeconds <= 0) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const current = highestByJid.get(jid);
|
|
39
|
+
if (current === undefined || readTimestampSeconds > current) {
|
|
40
|
+
highestByJid.set(jid, readTimestampSeconds);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (highestByJid.size === 0) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const watermarks = new Array(highestByJid.size);
|
|
47
|
+
let index = 0;
|
|
48
|
+
for (const [jid, readTimestampSeconds] of highestByJid) {
|
|
49
|
+
watermarks[index] = { jid, readTimestampSeconds };
|
|
50
|
+
index += 1;
|
|
51
|
+
}
|
|
52
|
+
return watermarks;
|
|
53
|
+
}
|
|
54
|
+
function parsePendingStatusMessages(node) {
|
|
55
|
+
const statusNode = findNodeChild(node, OFFLINE_THREAD_TAGS.STATUS_MSGS);
|
|
56
|
+
if (!statusNode) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const items = getNodeChildrenByTag(statusNode, OFFLINE_THREAD_TAGS.ITEM);
|
|
60
|
+
const jids = [];
|
|
61
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
62
|
+
const jid = items[index].attrs.from;
|
|
63
|
+
if (jid) {
|
|
64
|
+
jids.push(jid);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
count: parseOptionalInt(statusNode.attrs.count) ?? 0,
|
|
69
|
+
jids
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function parsePendingNotifications(node) {
|
|
73
|
+
const notificationsNode = findNodeChild(node, OFFLINE_THREAD_TAGS.NOTIFICATIONS);
|
|
74
|
+
if (!notificationsNode) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return { count: parseOptionalInt(notificationsNode.attrs.count) ?? 0 };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parses the offline preview manifest out of an `<ib>` bulletin. Every child
|
|
81
|
+
* is optional: a bare node yields an empty `threads` list.
|
|
82
|
+
*
|
|
83
|
+
* @param node - the `<thread_metadata>` node, not the enclosing `<ib>`
|
|
84
|
+
*/
|
|
85
|
+
export function parseOfflineThreadMetadata(node) {
|
|
86
|
+
return {
|
|
87
|
+
threads: parseThreadPreviews(node),
|
|
88
|
+
readWatermarks: parseReadWatermarks(node),
|
|
89
|
+
pendingStatusMessages: parsePendingStatusMessages(node),
|
|
90
|
+
pendingNotifications: parsePendingNotifications(node)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { WA_NODE_TAGS } from '../../protocol/nodes.js';
|
|
2
|
+
import { WA_PRIVACY_CATEGORY_TO_SETTING, WA_PRIVACY_SETTING_VALUES, WA_PRIVACY_TAGS } from '../../protocol/privacy.js';
|
|
3
|
+
import { findNodeChild, getNodeChildren, getNodeChildrenByTag } from '../../transport/node/helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Server categories the library intentionally drops: they are either
|
|
6
|
+
* region/product specific or not part of the modeled privacy surface.
|
|
7
|
+
*/
|
|
8
|
+
const IGNORED_SERVER_CATEGORIES = new Set([
|
|
9
|
+
'stickers',
|
|
10
|
+
'dependentaccountmessages',
|
|
11
|
+
'cover_photo',
|
|
12
|
+
'dependent_account_calling',
|
|
13
|
+
'groupcreation'
|
|
14
|
+
]);
|
|
15
|
+
const SETTING_VALUES = WA_PRIVACY_SETTING_VALUES;
|
|
16
|
+
/** Parses the `<privacy>` payload of a privacy `get` IQ result. */
|
|
17
|
+
export function parsePrivacySettings(result) {
|
|
18
|
+
const privacyNode = findNodeChild(result, WA_NODE_TAGS.PRIVACY);
|
|
19
|
+
if (!privacyNode) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
const settings = {};
|
|
23
|
+
const categories = getNodeChildrenByTag(privacyNode, WA_PRIVACY_TAGS.CATEGORY);
|
|
24
|
+
for (let i = 0; i < categories.length; i += 1) {
|
|
25
|
+
const node = categories[i];
|
|
26
|
+
const name = node.attrs.name;
|
|
27
|
+
const value = node.attrs.value;
|
|
28
|
+
if (!name || !value) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (IGNORED_SERVER_CATEGORIES.has(name)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const settingName = WA_PRIVACY_CATEGORY_TO_SETTING[name];
|
|
35
|
+
if (!settingName) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (SETTING_VALUES[settingName]?.includes(value) !== true) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
settings[settingName] = value;
|
|
42
|
+
}
|
|
43
|
+
return settings;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the `dhash` the server echoes back on a privacy `set` result. It is a
|
|
47
|
+
* millisecond timestamp acting as the version stamp of that category's
|
|
48
|
+
* disallowed list, and is only present while the category sits on
|
|
49
|
+
* `contact_blacklist`.
|
|
50
|
+
*/
|
|
51
|
+
export function parsePrivacyCategoryDhash(result, category) {
|
|
52
|
+
const privacyNode = findNodeChild(result, WA_NODE_TAGS.PRIVACY);
|
|
53
|
+
if (!privacyNode) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const categories = getNodeChildrenByTag(privacyNode, WA_PRIVACY_TAGS.CATEGORY);
|
|
57
|
+
for (let i = 0; i < categories.length; i += 1) {
|
|
58
|
+
const node = categories[i];
|
|
59
|
+
if (node.attrs.name !== category) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
return node.attrs.dhash ?? null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Parses the `<privacy><list>` payload of a disallowed-list `get` IQ result,
|
|
68
|
+
* returning `null` when the reply carries no `<list>` at all. The server omits
|
|
69
|
+
* it to mean "nothing to update" - either the category is not on
|
|
70
|
+
* `contact_blacklist`, or the `dhash` the client sent still matches - which is
|
|
71
|
+
* different from an empty list, so a caller reacting to changes can skip it.
|
|
72
|
+
*/
|
|
73
|
+
export function parseDisallowedListUpdate(result) {
|
|
74
|
+
const privacyNode = findNodeChild(result, WA_NODE_TAGS.PRIVACY);
|
|
75
|
+
if (!privacyNode) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const listNode = findNodeChild(privacyNode, WA_PRIVACY_TAGS.LIST);
|
|
79
|
+
if (!listNode) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const dhash = listNode.attrs.dhash;
|
|
83
|
+
const userNodes = getNodeChildrenByTag(listNode, WA_PRIVACY_TAGS.USER);
|
|
84
|
+
const jids = new Array(userNodes.length);
|
|
85
|
+
let jidsCount = 0;
|
|
86
|
+
for (let i = 0; i < userNodes.length; i += 1) {
|
|
87
|
+
const jid = userNodes[i].attrs.jid;
|
|
88
|
+
if (jid) {
|
|
89
|
+
jids[jidsCount] = jid;
|
|
90
|
+
jidsCount += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
jids.length = jidsCount;
|
|
94
|
+
return { jids, dhash };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Same read as {@link parseDisallowedListUpdate}, flattening the "nothing to
|
|
98
|
+
* update" reply into an empty list for callers that just want the current
|
|
99
|
+
* membership.
|
|
100
|
+
*/
|
|
101
|
+
export function parseDisallowedList(result) {
|
|
102
|
+
return parseDisallowedListUpdate(result) ?? { jids: [] };
|
|
103
|
+
}
|
|
104
|
+
/** Parses the `<list>` payload of a blocklist `get` IQ result. */
|
|
105
|
+
export function parseBlocklist(result) {
|
|
106
|
+
const listNode = findNodeChild(result, WA_NODE_TAGS.LIST);
|
|
107
|
+
if (!listNode) {
|
|
108
|
+
return { jids: [] };
|
|
109
|
+
}
|
|
110
|
+
const dhash = listNode.attrs.dhash;
|
|
111
|
+
const itemNodes = getNodeChildren(listNode);
|
|
112
|
+
const jids = new Array(itemNodes.length);
|
|
113
|
+
let jidsCount = 0;
|
|
114
|
+
for (let i = 0; i < itemNodes.length; i += 1) {
|
|
115
|
+
const jid = itemNodes[i].attrs.jid;
|
|
116
|
+
if (jid) {
|
|
117
|
+
jids[jidsCount] = jid;
|
|
118
|
+
jidsCount += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
jids.length = jidsCount;
|
|
122
|
+
return { jids, dhash };
|
|
123
|
+
}
|
|
@@ -39,6 +39,7 @@ export function createGroupMetadataCache(options) {
|
|
|
39
39
|
groupJid,
|
|
40
40
|
participants,
|
|
41
41
|
ephemeral: cached?.ephemeral,
|
|
42
|
+
ephemeralTrigger: cached?.ephemeralTrigger,
|
|
42
43
|
updatedAtMs: Date.now()
|
|
43
44
|
});
|
|
44
45
|
};
|
|
@@ -121,6 +122,7 @@ export function createGroupMetadataCache(options) {
|
|
|
121
122
|
groupJid,
|
|
122
123
|
participants,
|
|
123
124
|
ephemeral: queried.ephemeral,
|
|
125
|
+
ephemeralTrigger: queried.ephemeralTrigger,
|
|
124
126
|
updatedAtMs: Date.now()
|
|
125
127
|
});
|
|
126
128
|
return participants;
|
|
@@ -137,15 +139,22 @@ export function createGroupMetadataCache(options) {
|
|
|
137
139
|
const cached = await groupMetadataStore.getGroupMetadata(groupJid);
|
|
138
140
|
return cached?.ephemeral ?? null;
|
|
139
141
|
};
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
if (cached) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
142
|
+
const resolveEphemeralSettings = async (groupJid) => {
|
|
143
|
+
let cached = await groupMetadataStore.getGroupMetadata(groupJid);
|
|
144
|
+
if (!cached) {
|
|
145
|
+
await refreshParticipantUsers(groupJid);
|
|
146
|
+
cached = await groupMetadataStore.getGroupMetadata(groupJid);
|
|
147
|
+
}
|
|
148
|
+
const expirationSeconds = cached?.ephemeral;
|
|
149
|
+
if (expirationSeconds === undefined || expirationSeconds <= 0) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
expirationSeconds,
|
|
154
|
+
...(cached?.ephemeralTrigger !== undefined ? { trigger: cached.ephemeralTrigger } : {})
|
|
155
|
+
};
|
|
148
156
|
};
|
|
157
|
+
const resolveEphemeral = async (groupJid) => (await resolveEphemeralSettings(groupJid))?.expirationSeconds ?? null;
|
|
149
158
|
const applyGroupEvent = async (event) => {
|
|
150
159
|
const groupJid = resolveGroupJidForGroupCacheEvent(event);
|
|
151
160
|
if (!groupJid) {
|
|
@@ -161,13 +170,18 @@ export function createGroupMetadataCache(options) {
|
|
|
161
170
|
return;
|
|
162
171
|
}
|
|
163
172
|
const nextEphemeral = event.expirationSeconds;
|
|
164
|
-
|
|
173
|
+
const parsedTrigger = event.mode === undefined ? undefined : Number(event.mode);
|
|
174
|
+
const nextTrigger = parsedTrigger !== undefined && Number.isFinite(parsedTrigger)
|
|
175
|
+
? parsedTrigger
|
|
176
|
+
: cached.ephemeralTrigger;
|
|
177
|
+
if (cached.ephemeral === nextEphemeral && cached.ephemeralTrigger === nextTrigger) {
|
|
165
178
|
return;
|
|
166
179
|
}
|
|
167
180
|
await groupMetadataStore.upsertGroupMetadata({
|
|
168
181
|
groupJid,
|
|
169
182
|
participants: cached.participants,
|
|
170
183
|
ephemeral: nextEphemeral,
|
|
184
|
+
ephemeralTrigger: nextTrigger,
|
|
171
185
|
updatedAtMs: Date.now()
|
|
172
186
|
});
|
|
173
187
|
return;
|
|
@@ -182,6 +196,7 @@ export function createGroupMetadataCache(options) {
|
|
|
182
196
|
groupJid,
|
|
183
197
|
participants: participantUsers,
|
|
184
198
|
ephemeral: existing?.ephemeral,
|
|
199
|
+
ephemeralTrigger: existing?.ephemeralTrigger,
|
|
185
200
|
updatedAtMs: Date.now()
|
|
186
201
|
});
|
|
187
202
|
return;
|
|
@@ -234,6 +249,7 @@ export function createGroupMetadataCache(options) {
|
|
|
234
249
|
refreshParticipantUsers,
|
|
235
250
|
getEphemeral,
|
|
236
251
|
resolveEphemeral,
|
|
252
|
+
resolveEphemeralSettings,
|
|
237
253
|
mutateFromGroupEvent
|
|
238
254
|
};
|
|
239
255
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { assertMediaUploadStatus, buildMediaUploadUrl, parseMediaUploadJsonBody, selectMediaUploadHost } from '../media.js';
|
|
3
|
-
import { MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
|
|
2
|
+
import { assertMediaUploadStatus, buildMediaUploadUrl, parseMediaUploadJsonBody, performPlaintextMediaUpload, selectMediaUploadHost } from '../media.js';
|
|
3
|
+
import { MEDIA_UPLOAD_PATHS, NEWSLETTER_MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
|
|
4
4
|
import { WaMediaCrypto } from '../../media/crypto/WaMediaCrypto.js';
|
|
5
5
|
import { findFirstLink } from '../../message/addons/link-preview/detect.js';
|
|
6
6
|
import { proto } from '../../proto.js';
|
|
@@ -94,6 +94,9 @@ function buildInlineFields(thumbnail) {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
async function uploadHqFromBytes(deps, thumbnail) {
|
|
97
|
+
if (deps.surface === 'newsletter') {
|
|
98
|
+
return uploadPlaintextHq(deps, thumbnail.bytes, thumbnail);
|
|
99
|
+
}
|
|
97
100
|
const mediaKey = await WaMediaCrypto.generateMediaKey();
|
|
98
101
|
const encrypted = await WaMediaCrypto.encryptBytes('thumbnail-link', mediaKey, thumbnail.bytes, { sidecar: false });
|
|
99
102
|
const directPath = await dispatchUpload(deps, encrypted.ciphertextHmac, encrypted.ciphertextHmac.byteLength, encrypted.fileEncSha256);
|
|
@@ -108,6 +111,9 @@ async function uploadHqFromBytes(deps, thumbnail) {
|
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
113
|
async function uploadHqFromStream(deps, thumbnail) {
|
|
114
|
+
if (deps.surface === 'newsletter') {
|
|
115
|
+
return uploadPlaintextHq(deps, thumbnail.stream, thumbnail);
|
|
116
|
+
}
|
|
111
117
|
const mediaKey = await WaMediaCrypto.generateMediaKey();
|
|
112
118
|
const encrypted = await WaMediaCrypto.encryptToFile('thumbnail-link', mediaKey, thumbnail.stream, { sidecar: false, expectedFileSize: thumbnail.contentLength });
|
|
113
119
|
let readStream;
|
|
@@ -134,6 +140,27 @@ async function uploadHqFromStream(deps, thumbnail) {
|
|
|
134
140
|
await WaMediaCrypto.cleanupEncryptedFile(encrypted.filePath);
|
|
135
141
|
}
|
|
136
142
|
}
|
|
143
|
+
/** Uploads the HQ thumbnail unencrypted, so no `mediaKey` / `thumbnailEncSha256`. */
|
|
144
|
+
async function uploadPlaintextHq(deps, source, dimensions) {
|
|
145
|
+
const mediaConn = await deps.getMediaConn();
|
|
146
|
+
const upload = await performPlaintextMediaUpload({ mediaTransfer: deps.mediaTransfer, mediaConn, logger: deps.logger }, {
|
|
147
|
+
source,
|
|
148
|
+
path: NEWSLETTER_MEDIA_UPLOAD_PATHS['thumbnail-link'],
|
|
149
|
+
mimetype: 'image/jpeg',
|
|
150
|
+
logLabel: 'uploading newsletter link preview thumbnail'
|
|
151
|
+
});
|
|
152
|
+
assertMediaUploadStatus(upload.status, 'newsletter thumbnail-link upload');
|
|
153
|
+
const parsed = parseMediaUploadJsonBody(upload.responseBytes, 'newsletter thumbnail-link upload');
|
|
154
|
+
if (!parsed.direct_path) {
|
|
155
|
+
throw new Error('newsletter thumbnail-link upload response missing direct_path');
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
thumbnailDirectPath: parsed.direct_path,
|
|
159
|
+
thumbnailSha256: upload.fileSha256,
|
|
160
|
+
...(dimensions.width !== undefined ? { thumbnailWidth: dimensions.width } : {}),
|
|
161
|
+
...(dimensions.height !== undefined ? { thumbnailHeight: dimensions.height } : {})
|
|
162
|
+
};
|
|
163
|
+
}
|
|
137
164
|
async function dispatchUpload(deps, body, contentLength, fileEncSha256) {
|
|
138
165
|
const mediaConn = await deps.getMediaConn();
|
|
139
166
|
const host = selectMediaUploadHost(mediaConn);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { assertMediaUploadStatus, parseMediaUploadJsonBody, performPlaintextMediaUpload } from '../media.js';
|
|
2
2
|
import { NEWSLETTER_MEDIA_UPLOAD_PATHS } from '../../media/constants.js';
|
|
3
3
|
import { createStickerPackZipStream } from '../../media/sticker/sticker-pack.js';
|
|
4
|
+
import { buildExtendedTextWithPreview } from '../../message/addons/link-preview/builder.js';
|
|
4
5
|
import { applyContextInfo } from '../../message/context-info.js';
|
|
5
|
-
import { isSendEventMessage, isSendEventResponseMessage, isSendKeepMessage, isSendMediaMessage, isSendPinMessage, isSendPollMessage, isSendPollVoteMessage, isSendReactionMessage, isSendRevokeMessage, isSendTextMessage, resolveMessageTypeAttr, unwrapMessage } from '../../message/encode/content.js';
|
|
6
|
+
import { isSendEventMessage, isSendEventResponseMessage, isSendKeepMessage, isSendMediaMessage, isSendPinMessage, isSendPollMessage, isSendPollVoteMessage, isSendReactionMessage, isSendRevokeMessage, isSendTextMessage, resolveEncMediaType, resolveMessageTypeAttr, unwrapMessage } from '../../message/encode/content.js';
|
|
6
7
|
import { toStickerPackProtoStickers, toStickerPackZipEntries, validateStickerPackInput } from '../../message/kinds/sticker-pack.js';
|
|
7
8
|
import { proto } from '../../proto.js';
|
|
8
9
|
import { WA_ENC_MEDIA_TYPES } from '../../protocol/message.js';
|
|
9
10
|
import { base64ToBytes } from '../../util/bytes.js';
|
|
11
|
+
import { toError } from '../../util/primitives.js';
|
|
10
12
|
export async function uploadNewsletterMedia(options, input) {
|
|
11
13
|
const upload = await performPlaintextMediaUpload({
|
|
12
14
|
mediaTransfer: options.mediaTransfer,
|
|
@@ -44,22 +46,41 @@ function resolveSendMediaKind(content) {
|
|
|
44
46
|
return 'ptt';
|
|
45
47
|
return content.type;
|
|
46
48
|
}
|
|
49
|
+
/** Enc media types a channel publish accepts; anything else ships as text. */
|
|
50
|
+
const NEWSLETTER_MEDIA_TYPES = new Set([
|
|
51
|
+
WA_ENC_MEDIA_TYPES.AUDIO,
|
|
52
|
+
WA_ENC_MEDIA_TYPES.DOCUMENT,
|
|
53
|
+
WA_ENC_MEDIA_TYPES.GIF,
|
|
54
|
+
WA_ENC_MEDIA_TYPES.IMAGE,
|
|
55
|
+
WA_ENC_MEDIA_TYPES.PTT,
|
|
56
|
+
WA_ENC_MEDIA_TYPES.PTV,
|
|
57
|
+
WA_ENC_MEDIA_TYPES.STICKER,
|
|
58
|
+
WA_ENC_MEDIA_TYPES.STICKER_PACK,
|
|
59
|
+
WA_ENC_MEDIA_TYPES.URL,
|
|
60
|
+
WA_ENC_MEDIA_TYPES.VCARD,
|
|
61
|
+
WA_ENC_MEDIA_TYPES.VIDEO
|
|
62
|
+
]);
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the `plaintext` node `mediatype`. Shares the encrypted send path's
|
|
65
|
+
* resolver so media without an upload still gets it - a link preview publishes
|
|
66
|
+
* as `url`, a contact card as `vcard`; without it the channel drops the card.
|
|
67
|
+
*/
|
|
47
68
|
function pickMediaTypeFromMessage(message) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return
|
|
62
|
-
|
|
69
|
+
const mediaType = resolveEncMediaType(message);
|
|
70
|
+
return mediaType !== null && NEWSLETTER_MEDIA_TYPES.has(mediaType) ? mediaType : null;
|
|
71
|
+
}
|
|
72
|
+
async function resolveTextLinkPreview(options, content) {
|
|
73
|
+
if (!options.linkPreviewResolver)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
return await options.linkPreviewResolver(content);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
options.logger.warn('link preview resolver failed, sending plain text', {
|
|
80
|
+
message: toError(error).message
|
|
81
|
+
});
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
63
84
|
}
|
|
64
85
|
function buildMediaProtoMessage(content, upload) {
|
|
65
86
|
const common = {
|
|
@@ -145,11 +166,15 @@ export async function buildNewsletterMessageContent(options, content, ctx) {
|
|
|
145
166
|
};
|
|
146
167
|
}
|
|
147
168
|
if (isSendTextMessage(content)) {
|
|
148
|
-
const
|
|
169
|
+
const preview = await resolveTextLinkPreview(options, content);
|
|
170
|
+
const message = applyContextInfo(preview !== null
|
|
171
|
+
? buildExtendedTextWithPreview(content.text, preview.resolved, preview.thumbnailFields)
|
|
172
|
+
: { extendedTextMessage: { text: content.text } }, ctx);
|
|
173
|
+
const mediaType = pickMediaTypeFromMessage(message);
|
|
149
174
|
return {
|
|
150
|
-
kind: 'text',
|
|
175
|
+
kind: mediaType !== null ? 'media' : 'text',
|
|
151
176
|
plaintext: proto.Message.encode(message).finish(),
|
|
152
|
-
mediaType
|
|
177
|
+
mediaType,
|
|
153
178
|
upload: null
|
|
154
179
|
};
|
|
155
180
|
}
|
|
@@ -8,7 +8,8 @@ async function buildContent(deps, content, ctx) {
|
|
|
8
8
|
return buildNewsletterMessageContent({
|
|
9
9
|
logger: deps.logger,
|
|
10
10
|
mediaTransfer: deps.mediaTransfer,
|
|
11
|
-
getMediaConn: deps.getMediaConn
|
|
11
|
+
getMediaConn: deps.getMediaConn,
|
|
12
|
+
linkPreviewResolver: deps.linkPreviewResolver
|
|
12
13
|
}, content, ctx);
|
|
13
14
|
}
|
|
14
15
|
function toUploadSummary(built) {
|
|
@@ -9,7 +9,8 @@ function mergeThread(previous, incoming) {
|
|
|
9
9
|
pinned: incoming.pinned ?? previous.pinned,
|
|
10
10
|
muteEndMs: incoming.muteEndMs ?? previous.muteEndMs,
|
|
11
11
|
markedAsUnread: incoming.markedAsUnread ?? previous.markedAsUnread,
|
|
12
|
-
ephemeralExpiration: incoming.ephemeralExpiration ?? previous.ephemeralExpiration
|
|
12
|
+
ephemeralExpiration: incoming.ephemeralExpiration ?? previous.ephemeralExpiration,
|
|
13
|
+
ephemeralSettingTimestamp: incoming.ephemeralSettingTimestamp ?? previous.ephemeralSettingTimestamp
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
function mergeContact(previous, incoming) {
|