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.
Files changed (141) hide show
  1. package/dist/abprops-spec.d.ts +10 -0
  2. package/dist/abprops-spec.js +25 -0
  3. package/dist/client/WaClient.d.ts +11 -0
  4. package/dist/client/WaClient.js +60 -0
  5. package/dist/client/WaClientFactory.d.ts +2 -2
  6. package/dist/client/WaClientFactory.js +50 -12
  7. package/dist/client/coordinators/WaAbPropsCoordinator.d.ts +2 -2
  8. package/dist/client/coordinators/WaAbPropsCoordinator.js +16 -4
  9. package/dist/client/coordinators/WaIncomingNodeCoordinator.d.ts +2 -1
  10. package/dist/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  11. package/dist/client/coordinators/WaMessageCoordinator.d.ts +96 -0
  12. package/dist/client/coordinators/WaMessageCoordinator.js +163 -8
  13. package/dist/client/coordinators/WaMessageDispatchCoordinator.d.ts +64 -0
  14. package/dist/client/coordinators/WaMessageDispatchCoordinator.js +217 -30
  15. package/dist/client/coordinators/WaPrivacyCoordinator.d.ts +97 -19
  16. package/dist/client/coordinators/WaPrivacyCoordinator.js +152 -101
  17. package/dist/client/coordinators/WaRetryCoordinator.d.ts +22 -0
  18. package/dist/client/coordinators/WaRetryCoordinator.js +74 -11
  19. package/dist/client/events/dirty.d.ts +3 -0
  20. package/dist/client/events/dirty.js +12 -7
  21. package/dist/client/events/offline.d.ts +9 -0
  22. package/dist/client/events/offline.js +95 -0
  23. package/dist/client/events/privacy.d.ts +45 -0
  24. package/dist/client/events/privacy.js +130 -0
  25. package/dist/client/messaging/group-metadata.d.ts +12 -0
  26. package/dist/client/messaging/group-metadata.js +24 -8
  27. package/dist/client/messaging/link-preview.d.ts +7 -0
  28. package/dist/client/messaging/link-preview.js +27 -0
  29. package/dist/client/newsletter/content.d.ts +4 -1
  30. package/dist/client/newsletter/content.js +43 -18
  31. package/dist/client/newsletter/messaging.d.ts +2 -0
  32. package/dist/client/newsletter/messaging.js +2 -1
  33. package/dist/client/newsletter/mex.d.ts +2 -2
  34. package/dist/client/persistence/WriteBehindPersistence.js +2 -1
  35. package/dist/client/persistence/ephemeral-setting.d.ts +42 -0
  36. package/dist/client/persistence/ephemeral-setting.js +120 -0
  37. package/dist/client/persistence/group-history.d.ts +29 -0
  38. package/dist/client/persistence/group-history.js +149 -0
  39. package/dist/client/persistence/history-blob.d.ts +25 -0
  40. package/dist/client/persistence/history-blob.js +38 -0
  41. package/dist/client/persistence/history-sync.d.ts +2 -0
  42. package/dist/client/persistence/history-sync.js +31 -29
  43. package/dist/client/types.d.ts +150 -16
  44. package/dist/crypto/core/xeddsa.js +16 -0
  45. package/dist/crypto/curves/X25519.js +13 -0
  46. package/dist/crypto/nativeBackend.d.ts +20 -0
  47. package/dist/crypto/nativeBackend.js +95 -0
  48. package/dist/esm/abprops-spec.js +15 -0
  49. package/dist/esm/client/WaClient.js +60 -0
  50. package/dist/esm/client/WaClientFactory.js +51 -13
  51. package/dist/esm/client/coordinators/WaAbPropsCoordinator.js +17 -5
  52. package/dist/esm/client/coordinators/WaIncomingNodeCoordinator.js +4 -0
  53. package/dist/esm/client/coordinators/WaMessageCoordinator.js +166 -11
  54. package/dist/esm/client/coordinators/WaMessageDispatchCoordinator.js +218 -32
  55. package/dist/esm/client/coordinators/WaPrivacyCoordinator.js +145 -94
  56. package/dist/esm/client/coordinators/WaRetryCoordinator.js +74 -11
  57. package/dist/esm/client/events/dirty.js +12 -7
  58. package/dist/esm/client/events/offline.js +92 -0
  59. package/dist/esm/client/events/privacy.js +123 -0
  60. package/dist/esm/client/messaging/group-metadata.js +25 -9
  61. package/dist/esm/client/messaging/link-preview.js +29 -2
  62. package/dist/esm/client/newsletter/content.js +44 -19
  63. package/dist/esm/client/newsletter/messaging.js +2 -1
  64. package/dist/esm/client/persistence/WriteBehindPersistence.js +2 -1
  65. package/dist/esm/client/persistence/ephemeral-setting.js +116 -0
  66. package/dist/esm/client/persistence/group-history.js +145 -0
  67. package/dist/esm/client/persistence/history-blob.js +34 -0
  68. package/dist/esm/client/persistence/history-sync.js +24 -22
  69. package/dist/esm/crypto/core/xeddsa.js +17 -1
  70. package/dist/esm/crypto/curves/X25519.js +13 -0
  71. package/dist/esm/crypto/nativeBackend.js +93 -0
  72. package/dist/esm/index.js +3 -1
  73. package/dist/esm/message/context-info.js +39 -0
  74. package/dist/esm/message/encode/content.js +20 -4
  75. package/dist/esm/message/encode/media-payload.js +3 -1
  76. package/dist/esm/message/kinds/group-history.js +36 -0
  77. package/dist/esm/message/primitives/incoming.js +18 -6
  78. package/dist/esm/protocol/abprops.js +18 -162
  79. package/dist/esm/protocol/constants.js +4 -3
  80. package/dist/esm/protocol/defaults.js +6 -0
  81. package/dist/esm/protocol/media.js +1 -0
  82. package/dist/esm/protocol/message.js +8 -0
  83. package/dist/esm/protocol/notification.js +1 -0
  84. package/dist/esm/protocol/privacy.js +70 -4
  85. package/dist/esm/store/contracts/chat-metadata.store.js +1 -0
  86. package/dist/esm/store/createStore.js +19 -2
  87. package/dist/esm/store/index.js +1 -0
  88. package/dist/esm/store/memory/chat-metadata.store.js +66 -0
  89. package/dist/esm/store/noop.store.js +8 -0
  90. package/dist/esm/transport/node/builders/privacy.js +63 -4
  91. package/dist/esm/util/coercion.js +12 -4
  92. package/dist/esm/util/index.js +1 -1
  93. package/dist/index.d.ts +11 -6
  94. package/dist/index.js +15 -2
  95. package/dist/media/types.d.ts +1 -1
  96. package/dist/message/context-info.d.ts +10 -0
  97. package/dist/message/context-info.js +40 -0
  98. package/dist/message/encode/content.d.ts +7 -3
  99. package/dist/message/encode/content.js +20 -4
  100. package/dist/message/encode/media-payload.d.ts +5 -2
  101. package/dist/message/encode/media-payload.js +3 -1
  102. package/dist/message/kinds/group-history.d.ts +30 -0
  103. package/dist/message/kinds/group-history.js +40 -0
  104. package/dist/message/primitives/incoming.d.ts +5 -0
  105. package/dist/message/primitives/incoming.js +18 -6
  106. package/dist/protocol/abprops.d.ts +27 -140
  107. package/dist/protocol/abprops.js +18 -163
  108. package/dist/protocol/constants.d.ts +4 -3
  109. package/dist/protocol/constants.js +10 -3
  110. package/dist/protocol/defaults.d.ts +6 -0
  111. package/dist/protocol/defaults.js +6 -0
  112. package/dist/protocol/media.d.ts +1 -0
  113. package/dist/protocol/media.js +1 -0
  114. package/dist/protocol/message.d.ts +6 -0
  115. package/dist/protocol/message.js +9 -0
  116. package/dist/protocol/notification.d.ts +1 -0
  117. package/dist/protocol/notification.js +1 -0
  118. package/dist/protocol/privacy.d.ts +52 -0
  119. package/dist/protocol/privacy.js +71 -5
  120. package/dist/store/contracts/chat-metadata.store.d.ts +34 -0
  121. package/dist/store/contracts/chat-metadata.store.js +2 -0
  122. package/dist/store/contracts/group-metadata.store.d.ts +7 -0
  123. package/dist/store/contracts/thread.store.d.ts +6 -0
  124. package/dist/store/createStore.d.ts +10 -2
  125. package/dist/store/createStore.js +18 -1
  126. package/dist/store/index.d.ts +3 -1
  127. package/dist/store/index.js +3 -1
  128. package/dist/store/memory/chat-metadata.store.d.ts +26 -0
  129. package/dist/store/memory/chat-metadata.store.js +70 -0
  130. package/dist/store/noop.store.d.ts +2 -0
  131. package/dist/store/noop.store.js +9 -1
  132. package/dist/store/types.d.ts +190 -39
  133. package/dist/transport/node/builders/privacy.d.ts +26 -2
  134. package/dist/transport/node/builders/privacy.js +63 -3
  135. package/dist/util/coercion.d.ts +6 -0
  136. package/dist/util/coercion.js +13 -4
  137. package/dist/util/index.d.ts +1 -1
  138. package/dist/util/index.js +2 -1
  139. package/package.json +3 -1
  140. package/spec/abprops/index.d.ts +4402 -0
  141. package/spec/abprops/index.js +4372 -0
@@ -2,10 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseDirtyBits = parseDirtyBits;
4
4
  exports.handleDirtyBits = handleDirtyBits;
5
+ const privacy_1 = require("../events/privacy");
5
6
  const constants_1 = require("../../protocol/constants");
6
7
  const jid_1 = require("../../protocol/jid");
7
8
  const account_sync_1 = require("../../transport/node/builders/account-sync");
8
- const privacy_1 = require("../../transport/node/builders/privacy");
9
9
  const usync_1 = require("../../transport/node/builders/usync");
10
10
  const helpers_1 = require("../../transport/node/helpers");
11
11
  const query_1 = require("../../transport/node/query");
@@ -223,19 +223,23 @@ async function syncAccountPictureDirtyBit(runtime) {
223
223
  }
224
224
  throw new Error(`account_sync.picture iq failed (${iqError.code}: ${iqError.text})`);
225
225
  }
226
+ /**
227
+ * Catch-up path for a session that was offline while another device changed a
228
+ * setting. Runs the same refresh the live `account_sync` notification
229
+ * triggers: the whole category set plus the disallowed lists.
230
+ */
226
231
  async function syncAccountPrivacyDirtyBit(runtime) {
227
- await runSyncQuery(runtime, {
228
- queryContext: 'account_sync.privacy',
229
- node: (0, privacy_1.buildGetPrivacySettingsIq)(),
230
- logMessage: 'account_sync privacy synchronized'
231
- });
232
+ await runtime.syncAccountPrivacy();
233
+ runtime.logger.debug('account_sync privacy synchronized');
232
234
  }
235
+ /** The reply carries the full blocklist, not a delta. */
233
236
  async function syncAccountBlocklistDirtyBit(runtime) {
234
- await runSyncQuery(runtime, {
237
+ const response = await runSyncQuery(runtime, {
235
238
  queryContext: 'account_sync.blocklist',
236
239
  node: (0, account_sync_1.buildAccountBlocklistSyncIq)(),
237
240
  logMessage: 'account_sync blocklist synchronized'
238
241
  });
242
+ runtime.emitBlocklist((0, privacy_1.parseBlocklist)(response));
239
243
  }
240
244
  async function syncGroupsDirtyBit(runtime) {
241
245
  await runSyncQuery(runtime, {
@@ -275,6 +279,7 @@ async function runSyncQuery(runtime, args) {
275
279
  (0, query_1.assertIqResult)(response, args.assertContext ?? args.queryContext);
276
280
  (0, usync_2.logUsyncProtocolErrors)((0, usync_1.parseUsyncResultEnvelope)(response), runtime.logger, args.queryContext);
277
281
  runtime.logger.debug(args.logMessage, args.contextData);
282
+ return response;
278
283
  }
279
284
  async function clearDirtyBits(runtime, dirtyBits) {
280
285
  try {
@@ -0,0 +1,9 @@
1
+ import type { WaOfflineThreadMetadataEvent } from '../types';
2
+ import type { BinaryNode } from '../../transport/types';
3
+ /**
4
+ * Parses the offline preview manifest out of an `<ib>` bulletin. Every child
5
+ * is optional: a bare node yields an empty `threads` list.
6
+ *
7
+ * @param node - the `<thread_metadata>` node, not the enclosing `<ib>`
8
+ */
9
+ export declare function parseOfflineThreadMetadata(node: BinaryNode): WaOfflineThreadMetadataEvent;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseOfflineThreadMetadata = parseOfflineThreadMetadata;
4
+ const helpers_1 = require("../../transport/node/helpers");
5
+ const primitives_1 = require("../../util/primitives");
6
+ const OFFLINE_THREAD_TAGS = Object.freeze({
7
+ ITEM: 'item',
8
+ WATERMARK: 'watermark',
9
+ STATUS_MSGS: 'status_msgs',
10
+ NOTIFICATIONS: 'notifications'
11
+ });
12
+ function parseThreadPreviews(node) {
13
+ const items = (0, helpers_1.getNodeChildrenByTag)(node, OFFLINE_THREAD_TAGS.ITEM);
14
+ const previews = [];
15
+ for (let index = 0; index < items.length; index += 1) {
16
+ const item = items[index];
17
+ const jid = item.attrs.from;
18
+ const timestampSeconds = (0, primitives_1.parseOptionalInt)(item.attrs.t);
19
+ if (!jid || timestampSeconds === undefined || timestampSeconds < 0) {
20
+ continue;
21
+ }
22
+ previews.push({ jid, timestampSeconds });
23
+ }
24
+ return previews;
25
+ }
26
+ /** Keeps the highest `sts` per jid; non-positive means "no watermark", not "read at epoch". */
27
+ function parseReadWatermarks(node) {
28
+ const watermarkNode = (0, helpers_1.findNodeChild)(node, OFFLINE_THREAD_TAGS.WATERMARK);
29
+ if (!watermarkNode) {
30
+ return undefined;
31
+ }
32
+ const highestByJid = new Map();
33
+ const items = (0, helpers_1.getNodeChildrenByTag)(watermarkNode, OFFLINE_THREAD_TAGS.ITEM);
34
+ for (let index = 0; index < items.length; index += 1) {
35
+ const item = items[index];
36
+ const jid = item.attrs.from;
37
+ const readTimestampSeconds = (0, primitives_1.parseOptionalInt)(item.attrs.sts);
38
+ if (!jid || readTimestampSeconds === undefined || readTimestampSeconds <= 0) {
39
+ continue;
40
+ }
41
+ const current = highestByJid.get(jid);
42
+ if (current === undefined || readTimestampSeconds > current) {
43
+ highestByJid.set(jid, readTimestampSeconds);
44
+ }
45
+ }
46
+ if (highestByJid.size === 0) {
47
+ return undefined;
48
+ }
49
+ const watermarks = new Array(highestByJid.size);
50
+ let index = 0;
51
+ for (const [jid, readTimestampSeconds] of highestByJid) {
52
+ watermarks[index] = { jid, readTimestampSeconds };
53
+ index += 1;
54
+ }
55
+ return watermarks;
56
+ }
57
+ function parsePendingStatusMessages(node) {
58
+ const statusNode = (0, helpers_1.findNodeChild)(node, OFFLINE_THREAD_TAGS.STATUS_MSGS);
59
+ if (!statusNode) {
60
+ return undefined;
61
+ }
62
+ const items = (0, helpers_1.getNodeChildrenByTag)(statusNode, OFFLINE_THREAD_TAGS.ITEM);
63
+ const jids = [];
64
+ for (let index = 0; index < items.length; index += 1) {
65
+ const jid = items[index].attrs.from;
66
+ if (jid) {
67
+ jids.push(jid);
68
+ }
69
+ }
70
+ return {
71
+ count: (0, primitives_1.parseOptionalInt)(statusNode.attrs.count) ?? 0,
72
+ jids
73
+ };
74
+ }
75
+ function parsePendingNotifications(node) {
76
+ const notificationsNode = (0, helpers_1.findNodeChild)(node, OFFLINE_THREAD_TAGS.NOTIFICATIONS);
77
+ if (!notificationsNode) {
78
+ return undefined;
79
+ }
80
+ return { count: (0, primitives_1.parseOptionalInt)(notificationsNode.attrs.count) ?? 0 };
81
+ }
82
+ /**
83
+ * Parses the offline preview manifest out of an `<ib>` bulletin. Every child
84
+ * is optional: a bare node yields an empty `threads` list.
85
+ *
86
+ * @param node - the `<thread_metadata>` node, not the enclosing `<ib>`
87
+ */
88
+ function parseOfflineThreadMetadata(node) {
89
+ return {
90
+ threads: parseThreadPreviews(node),
91
+ readWatermarks: parseReadWatermarks(node),
92
+ pendingStatusMessages: parsePendingStatusMessages(node),
93
+ pendingNotifications: parsePendingNotifications(node)
94
+ };
95
+ }
@@ -0,0 +1,45 @@
1
+ import { type WaPrivacyCategory, type WaPrivacySettingName, type WaPrivacySettingValueMap } from '../../protocol/privacy';
2
+ import type { BinaryNode } from '../../transport/types';
3
+ /**
4
+ * Snapshot of every privacy category the server reports. Categories the
5
+ * library does not model (or that came back as `error`) are absent instead
6
+ * of being surfaced with a placeholder value.
7
+ */
8
+ export type WaPrivacySettings = {
9
+ readonly [K in WaPrivacySettingName]?: WaPrivacySettingValueMap[K];
10
+ };
11
+ /** Per-category deny-list (the JIDs excluded from a `contact_blacklist` setting). */
12
+ export interface WaPrivacyDisallowedListResult {
13
+ readonly jids: readonly string[];
14
+ readonly dhash?: string;
15
+ }
16
+ /** Account-wide blocklist. The server always sends the full list, never a delta. */
17
+ export interface WaBlocklistResult {
18
+ readonly jids: readonly string[];
19
+ readonly dhash?: string;
20
+ }
21
+ /** Parses the `<privacy>` payload of a privacy `get` IQ result. */
22
+ export declare function parsePrivacySettings(result: BinaryNode): WaPrivacySettings;
23
+ /**
24
+ * Reads the `dhash` the server echoes back on a privacy `set` result. It is a
25
+ * millisecond timestamp acting as the version stamp of that category's
26
+ * disallowed list, and is only present while the category sits on
27
+ * `contact_blacklist`.
28
+ */
29
+ export declare function parsePrivacyCategoryDhash(result: BinaryNode, category: WaPrivacyCategory): string | null;
30
+ /**
31
+ * Parses the `<privacy><list>` payload of a disallowed-list `get` IQ result,
32
+ * returning `null` when the reply carries no `<list>` at all. The server omits
33
+ * it to mean "nothing to update" - either the category is not on
34
+ * `contact_blacklist`, or the `dhash` the client sent still matches - which is
35
+ * different from an empty list, so a caller reacting to changes can skip it.
36
+ */
37
+ export declare function parseDisallowedListUpdate(result: BinaryNode): WaPrivacyDisallowedListResult | null;
38
+ /**
39
+ * Same read as {@link parseDisallowedListUpdate}, flattening the "nothing to
40
+ * update" reply into an empty list for callers that just want the current
41
+ * membership.
42
+ */
43
+ export declare function parseDisallowedList(result: BinaryNode): WaPrivacyDisallowedListResult;
44
+ /** Parses the `<list>` payload of a blocklist `get` IQ result. */
45
+ export declare function parseBlocklist(result: BinaryNode): WaBlocklistResult;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePrivacySettings = parsePrivacySettings;
4
+ exports.parsePrivacyCategoryDhash = parsePrivacyCategoryDhash;
5
+ exports.parseDisallowedListUpdate = parseDisallowedListUpdate;
6
+ exports.parseDisallowedList = parseDisallowedList;
7
+ exports.parseBlocklist = parseBlocklist;
8
+ const nodes_1 = require("../../protocol/nodes");
9
+ const privacy_1 = require("../../protocol/privacy");
10
+ const helpers_1 = require("../../transport/node/helpers");
11
+ /**
12
+ * Server categories the library intentionally drops: they are either
13
+ * region/product specific or not part of the modeled privacy surface.
14
+ */
15
+ const IGNORED_SERVER_CATEGORIES = new Set([
16
+ 'stickers',
17
+ 'dependentaccountmessages',
18
+ 'cover_photo',
19
+ 'dependent_account_calling',
20
+ 'groupcreation'
21
+ ]);
22
+ const SETTING_VALUES = privacy_1.WA_PRIVACY_SETTING_VALUES;
23
+ /** Parses the `<privacy>` payload of a privacy `get` IQ result. */
24
+ function parsePrivacySettings(result) {
25
+ const privacyNode = (0, helpers_1.findNodeChild)(result, nodes_1.WA_NODE_TAGS.PRIVACY);
26
+ if (!privacyNode) {
27
+ return {};
28
+ }
29
+ const settings = {};
30
+ const categories = (0, helpers_1.getNodeChildrenByTag)(privacyNode, privacy_1.WA_PRIVACY_TAGS.CATEGORY);
31
+ for (let i = 0; i < categories.length; i += 1) {
32
+ const node = categories[i];
33
+ const name = node.attrs.name;
34
+ const value = node.attrs.value;
35
+ if (!name || !value) {
36
+ continue;
37
+ }
38
+ if (IGNORED_SERVER_CATEGORIES.has(name)) {
39
+ continue;
40
+ }
41
+ const settingName = privacy_1.WA_PRIVACY_CATEGORY_TO_SETTING[name];
42
+ if (!settingName) {
43
+ continue;
44
+ }
45
+ if (SETTING_VALUES[settingName]?.includes(value) !== true) {
46
+ continue;
47
+ }
48
+ settings[settingName] = value;
49
+ }
50
+ return settings;
51
+ }
52
+ /**
53
+ * Reads the `dhash` the server echoes back on a privacy `set` result. It is a
54
+ * millisecond timestamp acting as the version stamp of that category's
55
+ * disallowed list, and is only present while the category sits on
56
+ * `contact_blacklist`.
57
+ */
58
+ function parsePrivacyCategoryDhash(result, category) {
59
+ const privacyNode = (0, helpers_1.findNodeChild)(result, nodes_1.WA_NODE_TAGS.PRIVACY);
60
+ if (!privacyNode) {
61
+ return null;
62
+ }
63
+ const categories = (0, helpers_1.getNodeChildrenByTag)(privacyNode, privacy_1.WA_PRIVACY_TAGS.CATEGORY);
64
+ for (let i = 0; i < categories.length; i += 1) {
65
+ const node = categories[i];
66
+ if (node.attrs.name !== category) {
67
+ continue;
68
+ }
69
+ return node.attrs.dhash ?? null;
70
+ }
71
+ return null;
72
+ }
73
+ /**
74
+ * Parses the `<privacy><list>` payload of a disallowed-list `get` IQ result,
75
+ * returning `null` when the reply carries no `<list>` at all. The server omits
76
+ * it to mean "nothing to update" - either the category is not on
77
+ * `contact_blacklist`, or the `dhash` the client sent still matches - which is
78
+ * different from an empty list, so a caller reacting to changes can skip it.
79
+ */
80
+ function parseDisallowedListUpdate(result) {
81
+ const privacyNode = (0, helpers_1.findNodeChild)(result, nodes_1.WA_NODE_TAGS.PRIVACY);
82
+ if (!privacyNode) {
83
+ return null;
84
+ }
85
+ const listNode = (0, helpers_1.findNodeChild)(privacyNode, privacy_1.WA_PRIVACY_TAGS.LIST);
86
+ if (!listNode) {
87
+ return null;
88
+ }
89
+ const dhash = listNode.attrs.dhash;
90
+ const userNodes = (0, helpers_1.getNodeChildrenByTag)(listNode, privacy_1.WA_PRIVACY_TAGS.USER);
91
+ const jids = new Array(userNodes.length);
92
+ let jidsCount = 0;
93
+ for (let i = 0; i < userNodes.length; i += 1) {
94
+ const jid = userNodes[i].attrs.jid;
95
+ if (jid) {
96
+ jids[jidsCount] = jid;
97
+ jidsCount += 1;
98
+ }
99
+ }
100
+ jids.length = jidsCount;
101
+ return { jids, dhash };
102
+ }
103
+ /**
104
+ * Same read as {@link parseDisallowedListUpdate}, flattening the "nothing to
105
+ * update" reply into an empty list for callers that just want the current
106
+ * membership.
107
+ */
108
+ function parseDisallowedList(result) {
109
+ return parseDisallowedListUpdate(result) ?? { jids: [] };
110
+ }
111
+ /** Parses the `<list>` payload of a blocklist `get` IQ result. */
112
+ function parseBlocklist(result) {
113
+ const listNode = (0, helpers_1.findNodeChild)(result, nodes_1.WA_NODE_TAGS.LIST);
114
+ if (!listNode) {
115
+ return { jids: [] };
116
+ }
117
+ const dhash = listNode.attrs.dhash;
118
+ const itemNodes = (0, helpers_1.getNodeChildren)(listNode);
119
+ const jids = new Array(itemNodes.length);
120
+ let jidsCount = 0;
121
+ for (let i = 0; i < itemNodes.length; i += 1) {
122
+ const jid = itemNodes[i].attrs.jid;
123
+ if (jid) {
124
+ jids[jidsCount] = jid;
125
+ jidsCount += 1;
126
+ }
127
+ }
128
+ jids.length = jidsCount;
129
+ return { jids, dhash };
130
+ }
@@ -4,12 +4,24 @@ import type { WaGroupMetadataStore } from '../../store/contracts/group-metadata.
4
4
  export type GroupMetadataQueryResult = {
5
5
  readonly participants: readonly string[];
6
6
  readonly ephemeral?: number;
7
+ readonly ephemeralTrigger?: number;
8
+ };
9
+ /** Disappearing-message settings a group reports, as sent on outgoing messages. */
10
+ export type GroupEphemeralSettings = {
11
+ readonly expirationSeconds: number;
12
+ readonly trigger?: number;
7
13
  };
8
14
  export type GroupMetadataCache = {
9
15
  resolveParticipantUsers(groupJid: string): Promise<readonly string[]>;
10
16
  refreshParticipantUsers(groupJid: string): Promise<readonly string[]>;
11
17
  getEphemeral(groupJid: string): Promise<number | null>;
12
18
  resolveEphemeral(groupJid: string): Promise<number | null>;
19
+ /**
20
+ * Expiration plus the group's `disappearingMode` trigger in one read, or
21
+ * `null` when the group is not ephemeral. Refreshes a cold cache like
22
+ * {@link resolveEphemeral} does.
23
+ */
24
+ resolveEphemeralSettings(groupJid: string): Promise<GroupEphemeralSettings | null>;
13
25
  mutateFromGroupEvent(event: WaGroupEvent): Promise<void>;
14
26
  };
15
27
  export declare function createGroupMetadataCache(options: {
@@ -42,6 +42,7 @@ function createGroupMetadataCache(options) {
42
42
  groupJid,
43
43
  participants,
44
44
  ephemeral: cached?.ephemeral,
45
+ ephemeralTrigger: cached?.ephemeralTrigger,
45
46
  updatedAtMs: Date.now()
46
47
  });
47
48
  };
@@ -124,6 +125,7 @@ function createGroupMetadataCache(options) {
124
125
  groupJid,
125
126
  participants,
126
127
  ephemeral: queried.ephemeral,
128
+ ephemeralTrigger: queried.ephemeralTrigger,
127
129
  updatedAtMs: Date.now()
128
130
  });
129
131
  return participants;
@@ -140,15 +142,22 @@ function createGroupMetadataCache(options) {
140
142
  const cached = await groupMetadataStore.getGroupMetadata(groupJid);
141
143
  return cached?.ephemeral ?? null;
142
144
  };
143
- const resolveEphemeral = async (groupJid) => {
144
- const cached = await groupMetadataStore.getGroupMetadata(groupJid);
145
- if (cached) {
146
- return cached.ephemeral ?? null;
145
+ const resolveEphemeralSettings = async (groupJid) => {
146
+ let cached = await groupMetadataStore.getGroupMetadata(groupJid);
147
+ if (!cached) {
148
+ await refreshParticipantUsers(groupJid);
149
+ cached = await groupMetadataStore.getGroupMetadata(groupJid);
150
+ }
151
+ const expirationSeconds = cached?.ephemeral;
152
+ if (expirationSeconds === undefined || expirationSeconds <= 0) {
153
+ return null;
147
154
  }
148
- await refreshParticipantUsers(groupJid);
149
- const refreshed = await groupMetadataStore.getGroupMetadata(groupJid);
150
- return refreshed?.ephemeral ?? null;
155
+ return {
156
+ expirationSeconds,
157
+ ...(cached?.ephemeralTrigger !== undefined ? { trigger: cached.ephemeralTrigger } : {})
158
+ };
151
159
  };
160
+ const resolveEphemeral = async (groupJid) => (await resolveEphemeralSettings(groupJid))?.expirationSeconds ?? null;
152
161
  const applyGroupEvent = async (event) => {
153
162
  const groupJid = resolveGroupJidForGroupCacheEvent(event);
154
163
  if (!groupJid) {
@@ -164,13 +173,18 @@ function createGroupMetadataCache(options) {
164
173
  return;
165
174
  }
166
175
  const nextEphemeral = event.expirationSeconds;
167
- if (cached.ephemeral === nextEphemeral) {
176
+ const parsedTrigger = event.mode === undefined ? undefined : Number(event.mode);
177
+ const nextTrigger = parsedTrigger !== undefined && Number.isFinite(parsedTrigger)
178
+ ? parsedTrigger
179
+ : cached.ephemeralTrigger;
180
+ if (cached.ephemeral === nextEphemeral && cached.ephemeralTrigger === nextTrigger) {
168
181
  return;
169
182
  }
170
183
  await groupMetadataStore.upsertGroupMetadata({
171
184
  groupJid,
172
185
  participants: cached.participants,
173
186
  ephemeral: nextEphemeral,
187
+ ephemeralTrigger: nextTrigger,
174
188
  updatedAtMs: Date.now()
175
189
  });
176
190
  return;
@@ -185,6 +199,7 @@ function createGroupMetadataCache(options) {
185
199
  groupJid,
186
200
  participants: participantUsers,
187
201
  ephemeral: existing?.ephemeral,
202
+ ephemeralTrigger: existing?.ephemeralTrigger,
188
203
  updatedAtMs: Date.now()
189
204
  });
190
205
  return;
@@ -237,6 +252,7 @@ function createGroupMetadataCache(options) {
237
252
  refreshParticipantUsers,
238
253
  getEphemeral,
239
254
  resolveEphemeral,
255
+ resolveEphemeralSettings,
240
256
  mutateFromGroupEvent
241
257
  };
242
258
  }
@@ -4,6 +4,11 @@ import type { WaMediaConn } from '../../media/types';
4
4
  import type { WaLinkPreviewThumbnailFields } from '../../message/addons/link-preview/builder';
5
5
  import type { WaLinkPreviewFetcher, WaLinkPreviewOptions, WaLinkPreviewOverride, WaLinkPreviewResolved } from '../../message/addons/link-preview/types';
6
6
  import type { ServerClock } from '../../util/clock';
7
+ /**
8
+ * Where the preview is published. `chat` uploads the HQ thumbnail encrypted;
9
+ * `newsletter` uploads it in the clear, since channel media is not encrypted.
10
+ */
11
+ export type WaLinkPreviewSurface = 'chat' | 'newsletter';
7
12
  export interface ResolveLinkPreviewDeps {
8
13
  readonly logger: Logger;
9
14
  readonly mediaTransfer: WaMediaTransferClient;
@@ -11,6 +16,8 @@ export interface ResolveLinkPreviewDeps {
11
16
  readonly fetcher: WaLinkPreviewFetcher;
12
17
  readonly options: WaLinkPreviewOptions;
13
18
  readonly serverClock: ServerClock;
19
+ /** Defaults to `chat`. */
20
+ readonly surface?: WaLinkPreviewSurface;
14
21
  }
15
22
  export interface ResolvedLinkPreviewResult {
16
23
  readonly resolved: WaLinkPreviewResolved;
@@ -97,6 +97,9 @@ function buildInlineFields(thumbnail) {
97
97
  };
98
98
  }
99
99
  async function uploadHqFromBytes(deps, thumbnail) {
100
+ if (deps.surface === 'newsletter') {
101
+ return uploadPlaintextHq(deps, thumbnail.bytes, thumbnail);
102
+ }
100
103
  const mediaKey = await WaMediaCrypto_1.WaMediaCrypto.generateMediaKey();
101
104
  const encrypted = await WaMediaCrypto_1.WaMediaCrypto.encryptBytes('thumbnail-link', mediaKey, thumbnail.bytes, { sidecar: false });
102
105
  const directPath = await dispatchUpload(deps, encrypted.ciphertextHmac, encrypted.ciphertextHmac.byteLength, encrypted.fileEncSha256);
@@ -111,6 +114,9 @@ async function uploadHqFromBytes(deps, thumbnail) {
111
114
  };
112
115
  }
113
116
  async function uploadHqFromStream(deps, thumbnail) {
117
+ if (deps.surface === 'newsletter') {
118
+ return uploadPlaintextHq(deps, thumbnail.stream, thumbnail);
119
+ }
114
120
  const mediaKey = await WaMediaCrypto_1.WaMediaCrypto.generateMediaKey();
115
121
  const encrypted = await WaMediaCrypto_1.WaMediaCrypto.encryptToFile('thumbnail-link', mediaKey, thumbnail.stream, { sidecar: false, expectedFileSize: thumbnail.contentLength });
116
122
  let readStream;
@@ -137,6 +143,27 @@ async function uploadHqFromStream(deps, thumbnail) {
137
143
  await WaMediaCrypto_1.WaMediaCrypto.cleanupEncryptedFile(encrypted.filePath);
138
144
  }
139
145
  }
146
+ /** Uploads the HQ thumbnail unencrypted, so no `mediaKey` / `thumbnailEncSha256`. */
147
+ async function uploadPlaintextHq(deps, source, dimensions) {
148
+ const mediaConn = await deps.getMediaConn();
149
+ const upload = await (0, media_1.performPlaintextMediaUpload)({ mediaTransfer: deps.mediaTransfer, mediaConn, logger: deps.logger }, {
150
+ source,
151
+ path: constants_1.NEWSLETTER_MEDIA_UPLOAD_PATHS['thumbnail-link'],
152
+ mimetype: 'image/jpeg',
153
+ logLabel: 'uploading newsletter link preview thumbnail'
154
+ });
155
+ (0, media_1.assertMediaUploadStatus)(upload.status, 'newsletter thumbnail-link upload');
156
+ const parsed = (0, media_1.parseMediaUploadJsonBody)(upload.responseBytes, 'newsletter thumbnail-link upload');
157
+ if (!parsed.direct_path) {
158
+ throw new Error('newsletter thumbnail-link upload response missing direct_path');
159
+ }
160
+ return {
161
+ thumbnailDirectPath: parsed.direct_path,
162
+ thumbnailSha256: upload.fileSha256,
163
+ ...(dimensions.width !== undefined ? { thumbnailWidth: dimensions.width } : {}),
164
+ ...(dimensions.height !== undefined ? { thumbnailHeight: dimensions.height } : {})
165
+ };
166
+ }
140
167
  async function dispatchUpload(deps, body, contentLength, fileEncSha256) {
141
168
  const mediaConn = await deps.getMediaConn();
142
169
  const host = (0, media_1.selectMediaUploadHost)(mediaConn);
@@ -1,10 +1,11 @@
1
1
  import { type WaUploadMediaSource } from '../media';
2
+ import type { ResolvedLinkPreviewResult } from '../messaging/link-preview';
2
3
  import type { Logger } from '../../infra/log/types';
3
4
  import { type NewsletterMediaKind } from '../../media/constants';
4
5
  import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
5
6
  import type { WaMediaConn } from '../../media/types';
6
7
  import { type WaSendContextInfo } from '../../message/context-info';
7
- import type { WaSendMessageContent } from '../../message/types';
8
+ import type { WaSendMessageContent, WaSendTextMessage } from '../../message/types';
8
9
  export type WaNewsletterUploadMedia = WaUploadMediaSource;
9
10
  export interface WaNewsletterUploadInput {
10
11
  readonly mediaKind: NewsletterMediaKind;
@@ -38,5 +39,7 @@ export interface BuildNewsletterContentOptions {
38
39
  readonly logger: Logger;
39
40
  readonly mediaTransfer?: WaMediaTransferClient;
40
41
  readonly getMediaConn?: () => Promise<WaMediaConn>;
42
+ /** Must be bound to the `newsletter` surface (unencrypted thumbnail upload). */
43
+ readonly linkPreviewResolver?: (content: WaSendTextMessage) => Promise<ResolvedLinkPreviewResult | null>;
41
44
  }
42
45
  export declare function buildNewsletterMessageContent(options: BuildNewsletterContentOptions, content: WaSendMessageContent, ctx?: WaSendContextInfo | null): Promise<WaNewsletterBuiltContent>;
@@ -5,12 +5,14 @@ exports.buildNewsletterMessageContent = buildNewsletterMessageContent;
5
5
  const media_1 = require("../media");
6
6
  const constants_1 = require("../../media/constants");
7
7
  const sticker_pack_1 = require("../../media/sticker/sticker-pack");
8
+ const builder_1 = require("../../message/addons/link-preview/builder");
8
9
  const context_info_1 = require("../../message/context-info");
9
10
  const content_1 = require("../../message/encode/content");
10
11
  const sticker_pack_2 = require("../../message/kinds/sticker-pack");
11
12
  const _proto_1 = require("../../proto");
12
13
  const message_1 = require("../../protocol/message");
13
14
  const bytes_1 = require("../../util/bytes");
15
+ const primitives_1 = require("../../util/primitives");
14
16
  async function uploadNewsletterMedia(options, input) {
15
17
  const upload = await (0, media_1.performPlaintextMediaUpload)({
16
18
  mediaTransfer: options.mediaTransfer,
@@ -48,22 +50,41 @@ function resolveSendMediaKind(content) {
48
50
  return 'ptt';
49
51
  return content.type;
50
52
  }
53
+ /** Enc media types a channel publish accepts; anything else ships as text. */
54
+ const NEWSLETTER_MEDIA_TYPES = new Set([
55
+ message_1.WA_ENC_MEDIA_TYPES.AUDIO,
56
+ message_1.WA_ENC_MEDIA_TYPES.DOCUMENT,
57
+ message_1.WA_ENC_MEDIA_TYPES.GIF,
58
+ message_1.WA_ENC_MEDIA_TYPES.IMAGE,
59
+ message_1.WA_ENC_MEDIA_TYPES.PTT,
60
+ message_1.WA_ENC_MEDIA_TYPES.PTV,
61
+ message_1.WA_ENC_MEDIA_TYPES.STICKER,
62
+ message_1.WA_ENC_MEDIA_TYPES.STICKER_PACK,
63
+ message_1.WA_ENC_MEDIA_TYPES.URL,
64
+ message_1.WA_ENC_MEDIA_TYPES.VCARD,
65
+ message_1.WA_ENC_MEDIA_TYPES.VIDEO
66
+ ]);
67
+ /**
68
+ * Resolves the `plaintext` node `mediatype`. Shares the encrypted send path's
69
+ * resolver so media without an upload still gets it - a link preview publishes
70
+ * as `url`, a contact card as `vcard`; without it the channel drops the card.
71
+ */
51
72
  function pickMediaTypeFromMessage(message) {
52
- if (message.imageMessage)
53
- return message_1.WA_ENC_MEDIA_TYPES.IMAGE;
54
- if (message.videoMessage)
55
- return message.videoMessage.gifPlayback ? message_1.WA_ENC_MEDIA_TYPES.GIF : message_1.WA_ENC_MEDIA_TYPES.VIDEO;
56
- if (message.audioMessage)
57
- return message.audioMessage.ptt ? message_1.WA_ENC_MEDIA_TYPES.PTT : message_1.WA_ENC_MEDIA_TYPES.AUDIO;
58
- if (message.documentMessage)
59
- return message_1.WA_ENC_MEDIA_TYPES.DOCUMENT;
60
- if (message.stickerMessage)
61
- return message_1.WA_ENC_MEDIA_TYPES.STICKER;
62
- if (message.stickerPackMessage)
63
- return message_1.WA_ENC_MEDIA_TYPES.STICKER_PACK;
64
- if (message.ptvMessage)
65
- return message_1.WA_ENC_MEDIA_TYPES.PTV;
66
- return null;
73
+ const mediaType = (0, content_1.resolveEncMediaType)(message);
74
+ return mediaType !== null && NEWSLETTER_MEDIA_TYPES.has(mediaType) ? mediaType : null;
75
+ }
76
+ async function resolveTextLinkPreview(options, content) {
77
+ if (!options.linkPreviewResolver)
78
+ return null;
79
+ try {
80
+ return await options.linkPreviewResolver(content);
81
+ }
82
+ catch (error) {
83
+ options.logger.warn('link preview resolver failed, sending plain text', {
84
+ message: (0, primitives_1.toError)(error).message
85
+ });
86
+ return null;
87
+ }
67
88
  }
68
89
  function buildMediaProtoMessage(content, upload) {
69
90
  const common = {
@@ -149,11 +170,15 @@ async function buildNewsletterMessageContent(options, content, ctx) {
149
170
  };
150
171
  }
151
172
  if ((0, content_1.isSendTextMessage)(content)) {
152
- const message = (0, context_info_1.applyContextInfo)({ extendedTextMessage: { text: content.text } }, ctx);
173
+ const preview = await resolveTextLinkPreview(options, content);
174
+ const message = (0, context_info_1.applyContextInfo)(preview !== null
175
+ ? (0, builder_1.buildExtendedTextWithPreview)(content.text, preview.resolved, preview.thumbnailFields)
176
+ : { extendedTextMessage: { text: content.text } }, ctx);
177
+ const mediaType = pickMediaTypeFromMessage(message);
153
178
  return {
154
- kind: 'text',
179
+ kind: mediaType !== null ? 'media' : 'text',
155
180
  plaintext: _proto_1.proto.Message.encode(message).finish(),
156
- mediaType: null,
181
+ mediaType,
157
182
  upload: null
158
183
  };
159
184
  }
@@ -1,3 +1,4 @@
1
+ import { type BuildNewsletterContentOptions } from '../newsletter/content';
1
2
  import { type WaNewsletterMexDeps } from '../newsletter/mex';
2
3
  import type { WaNewsletterMuteInput, WaNewsletterReactInput, WaNewsletterRevokeInput, WaNewsletterSendOptions, WaNewsletterSendResult, WaNewsletterViewReceiptInput, WaNewsletterVotePollInput } from '../newsletter/types';
3
4
  import type { WaMediaTransferClient } from '../../media/transfer/WaMediaTransferClient';
@@ -10,6 +11,7 @@ export interface WaNewsletterMessagingDeps extends WaNewsletterMexDeps {
10
11
  readonly generateStanzaId: () => Promise<string>;
11
12
  readonly mediaTransfer?: WaMediaTransferClient;
12
13
  readonly getMediaConn?: () => Promise<WaMediaConn>;
14
+ readonly linkPreviewResolver?: BuildNewsletterContentOptions['linkPreviewResolver'];
13
15
  }
14
16
  /** Newsletter messaging operations (send, react, fetch, follow). */
15
17
  export interface WaNewsletterMessagingOps {
@@ -11,7 +11,8 @@ async function buildContent(deps, content, ctx) {
11
11
  return (0, content_1.buildNewsletterMessageContent)({
12
12
  logger: deps.logger,
13
13
  mediaTransfer: deps.mediaTransfer,
14
- getMediaConn: deps.getMediaConn
14
+ getMediaConn: deps.getMediaConn,
15
+ linkPreviewResolver: deps.linkPreviewResolver
15
16
  }, content, ctx);
16
17
  }
17
18
  function toUploadSummary(built) {
@@ -1,13 +1,13 @@
1
+ import type { WaAbPropName } from '../../abprops-spec';
1
2
  import type { Logger } from '../../infra/log/types';
2
3
  import type { WaMexOperationResponses, WaMexOperationVariables } from '../../mex';
3
- import type { AbPropName } from '../../protocol/abprops';
4
4
  import { type WaMexOpKey, type WaMexQuerySocket } from '../../transport/node/mex/client';
5
5
  import type { BinaryNode } from '../../transport/types';
6
6
  export type WaNewsletterTosKind = 'creation' | 'consumer' | 'admin_invite';
7
7
  export interface WaNewsletterMexDeps {
8
8
  readonly mexSocket: WaMexQuerySocket;
9
9
  readonly queryWithContext?: (context: string, node: BinaryNode, timeoutMs?: number) => Promise<BinaryNode>;
10
- readonly getAbPropString?: (name: AbPropName) => string;
10
+ readonly getAbPropString?: (name: WaAbPropName) => string;
11
11
  readonly logger: Logger;
12
12
  }
13
13
  export declare function runMex<K extends WaMexOpKey>(deps: WaNewsletterMexDeps, opKey: K, variables: WaMexOperationVariables[K]): Promise<WaMexOperationResponses[K] | null>;