@oxidezap/baileyrs 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/lib/Bridge/primitives.d.ts +49 -3
- package/lib/Bridge/primitives.js +126 -8
- package/lib/Bridge/schema.js +208 -87
- package/lib/Bridge/types.d.ts +61 -2
- package/lib/Compatibility/derived-stanza-nodes.d.ts +28 -0
- package/lib/Compatibility/derived-stanza-nodes.js +71 -0
- package/lib/Compatibility/encode-proto.d.ts +17 -0
- package/lib/Compatibility/encode-proto.js +32 -0
- package/lib/Compatibility/proto-runtime.d.ts +10 -0
- package/lib/Compatibility/proto-runtime.js +176 -11
- package/lib/Defaults/index.d.ts +6 -0
- package/lib/Defaults/index.js +6 -0
- package/lib/Socket/blocking.d.ts +3 -1
- package/lib/Socket/blocking.js +3 -0
- package/lib/Socket/communities.d.ts +14 -9
- package/lib/Socket/communities.js +24 -5
- package/lib/Socket/contacts.d.ts +3 -1
- package/lib/Socket/contacts.js +3 -0
- package/lib/Socket/events.js +35 -6
- package/lib/Socket/groups.d.ts +30 -4
- package/lib/Socket/groups.js +20 -4
- package/lib/Socket/index.d.ts +18 -17
- package/lib/Socket/index.js +18 -6
- package/lib/Socket/messages.js +18 -7
- package/lib/Socket/newsletter.d.ts +3 -1
- package/lib/Socket/newsletter.js +3 -3
- package/lib/Socket/presence.d.ts +3 -2
- package/lib/Socket/presence.js +4 -0
- package/lib/Socket/privacy.d.ts +7 -1
- package/lib/Socket/privacy.js +16 -0
- package/lib/Socket/server-queries.d.ts +3 -1
- package/lib/Socket/server-queries.js +3 -0
- package/lib/Types/Auth.d.ts +4 -1
- package/lib/Types/Chat.d.ts +26 -8
- package/lib/Types/Chat.js +18 -0
- package/lib/Types/Events.d.ts +17 -0
- package/lib/Types/GroupMetadata.d.ts +3 -1
- package/lib/Types/GroupMetadata.js +1 -1
- package/lib/Types/Message.d.ts +20 -2
- package/lib/Types/Message.js +11 -0
- package/lib/Utils/argument-domain.d.ts +15 -0
- package/lib/Utils/argument-domain.js +36 -0
- package/lib/Utils/event-buffer.js +48 -11
- package/lib/Utils/messages.d.ts +3 -1
- package/lib/Utils/messages.js +35 -7
- package/lib/Utils/process-history-message.d.ts +11 -2
- package/lib/Utils/process-history-message.js +11 -7
- package/lib/Utils/use-multi-file-auth-state.js +45 -0
- package/package.json +9 -3
package/lib/Types/Chat.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* privacy settings in WhatsApp Web
|
|
3
|
+
*
|
|
4
|
+
* Each of these is a set first and a type second: the wrappers that take one
|
|
5
|
+
* check the value against the same array the type is derived from, so a value
|
|
6
|
+
* cannot be accepted by the compiler and refused at runtime, or the reverse.
|
|
7
|
+
*/
|
|
8
|
+
export const WA_PRIVACY_VALUES = ['all', 'contacts', 'contact_blacklist', 'none'];
|
|
9
|
+
export const WA_PRIVACY_ONLINE_VALUES = ['all', 'match_last_seen'];
|
|
10
|
+
export const WA_PRIVACY_GROUP_ADD_VALUES = ['all', 'contacts', 'contact_blacklist'];
|
|
11
|
+
export const WA_READ_RECEIPTS_VALUES = ['all', 'none'];
|
|
12
|
+
export const WA_PRIVACY_CALL_VALUES = ['all', 'known'];
|
|
13
|
+
export const WA_PRIVACY_MESSAGES_VALUES = ['all', 'contacts'];
|
|
14
|
+
/** the two the account itself broadcasts, as opposed to the per-chat states */
|
|
15
|
+
export const WA_PRESENCE_STATUSES = ['unavailable', 'available'];
|
|
16
|
+
export const WA_CHAT_STATES = ['composing', 'recording', 'paused'];
|
|
17
|
+
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
|
|
18
|
+
export const WA_PRESENCES = [...WA_PRESENCE_STATUSES, ...WA_CHAT_STATES];
|
|
1
19
|
export const ALL_WA_PATCH_NAMES = [
|
|
2
20
|
'critical_block',
|
|
3
21
|
'critical_unblock_low',
|
package/lib/Types/Events.d.ts
CHANGED
|
@@ -132,6 +132,23 @@ export type BaileysEventMap = {
|
|
|
132
132
|
association: LabelAssociation;
|
|
133
133
|
type: 'add' | 'remove';
|
|
134
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* A batched app-state sync left collections unsynced. Not an upstream event:
|
|
137
|
+
* upstream never withholds a session on app state, so it has nothing to
|
|
138
|
+
* report here.
|
|
139
|
+
*
|
|
140
|
+
* The engine does the opposite of withholding — it announces a connection
|
|
141
|
+
* whose critical sync came back degraded, precisely so a session that works
|
|
142
|
+
* is usable — and this is what says which collections are missing from it.
|
|
143
|
+
* `connected` tells "degraded but usable" from a sync that ran before the
|
|
144
|
+
* connection was ready; `fatal` is the half a retry cannot fix.
|
|
145
|
+
*/
|
|
146
|
+
'app-state-sync.failed': {
|
|
147
|
+
fatal: string[];
|
|
148
|
+
retryable: string[];
|
|
149
|
+
skipped: string[];
|
|
150
|
+
connected: boolean;
|
|
151
|
+
};
|
|
135
152
|
/** Newsletter-related events */
|
|
136
153
|
'newsletter.reaction': {
|
|
137
154
|
id: string;
|
|
@@ -5,7 +5,9 @@ export type GroupParticipant = Contact & {
|
|
|
5
5
|
isSuperAdmin?: boolean;
|
|
6
6
|
admin?: 'admin' | 'superadmin' | null;
|
|
7
7
|
};
|
|
8
|
-
export
|
|
8
|
+
export declare const PARTICIPANT_ACTIONS: readonly ['add', 'remove', 'promote', 'demote', 'modify'];
|
|
9
|
+
/** Derived from the values, so the runtime check and the type cannot drift. */
|
|
10
|
+
export type ParticipantAction = (typeof PARTICIPANT_ACTIONS)[number];
|
|
9
11
|
export type RequestJoinAction = 'created' | 'revoked' | 'rejected';
|
|
10
12
|
export type RequestJoinMethod = 'invite_link' | 'linked_group_join' | 'non_admin_add' | undefined;
|
|
11
13
|
export interface GroupMetadata {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const PARTICIPANT_ACTIONS = ['add', 'remove', 'promote', 'demote', 'modify'];
|
|
2
2
|
//# sourceMappingURL=GroupMetadata.js.map
|
package/lib/Types/Message.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type Long from 'long';
|
|
1
2
|
import type { Readable } from 'stream';
|
|
2
3
|
import type { URL } from 'url';
|
|
3
4
|
import type { MediaType as BridgeMediaType, UploadMediaResult, WasmWhatsAppClient } from '@oxidezap/whatsapp-rust-bridge';
|
|
@@ -8,8 +9,23 @@ import type { BinaryNode } from './BinaryNode.js';
|
|
|
8
9
|
import type { GroupMetadata } from './GroupMetadata.js';
|
|
9
10
|
import type { CacheStore } from './Socket.js';
|
|
10
11
|
export { proto as WAProto };
|
|
11
|
-
export type WAMessage = Omit<proto.IWebMessageInfo, 'messageStubParameters'> & {
|
|
12
|
+
export type WAMessage = Omit<proto.IWebMessageInfo, 'messageStubParameters' | 'messageTimestamp'> & {
|
|
12
13
|
key: WAMessageKey;
|
|
14
|
+
/**
|
|
15
|
+
* `number | Long`, as upstream declares it — not the neutral codec's `Int64`.
|
|
16
|
+
*
|
|
17
|
+
* From bridge 0.8.0 a 64-bit field is typed `number | { low, high, unsigned }`,
|
|
18
|
+
* a plain data shape carrying none of Long's methods. That is what the *codec*
|
|
19
|
+
* produces; it is not what this library hands out. The compatibility facade
|
|
20
|
+
* supplies a reader that materialises every 64-bit word as a long.js Long
|
|
21
|
+
* whatever its magnitude, and the published declaration has always said so, so
|
|
22
|
+
* a consumer calling `.toNumber()` is right to expect one.
|
|
23
|
+
*
|
|
24
|
+
* Declared here rather than left to flow through, because the neutral shape
|
|
25
|
+
* otherwise reaches every type derived from `WAMessage` and stops them being
|
|
26
|
+
* assignable to upstream's.
|
|
27
|
+
*/
|
|
28
|
+
messageTimestamp?: number | Long | null;
|
|
13
29
|
category?: string;
|
|
14
30
|
retryCount?: number;
|
|
15
31
|
messageStubParameters?: any;
|
|
@@ -51,7 +67,9 @@ export type MessageType = keyof proto.Message;
|
|
|
51
67
|
export declare const WAMessageAddressingMode: typeof WAMessageAddressingModeType;
|
|
52
68
|
export type WAMessageAddressingMode = WAMessageAddressingModeType;
|
|
53
69
|
export type MessageWithContextInfo = 'imageMessage' | 'contactMessage' | 'locationMessage' | 'extendedTextMessage' | 'documentMessage' | 'audioMessage' | 'videoMessage' | 'call' | 'contactsArrayMessage' | 'liveLocationMessage' | 'templateMessage' | 'stickerMessage' | 'groupInviteMessage' | 'templateButtonReplyMessage' | 'productMessage' | 'listMessage' | 'orderMessage' | 'listResponseMessage' | 'buttonsMessage' | 'buttonsResponseMessage' | 'interactiveMessage' | 'interactiveResponseMessage' | 'pollCreationMessage' | 'requestPhoneNumberMessage' | 'messageHistoryBundle' | 'eventMessage' | 'newsletterAdminInviteMessage' | 'albumMessage' | 'stickerPackMessage' | 'pollResultSnapshotMessage' | 'messageHistoryNotice';
|
|
54
|
-
|
|
70
|
+
/** `undefined` is a member: it is how upstream spells a delivery receipt. */
|
|
71
|
+
export declare const MESSAGE_RECEIPT_TYPES: readonly ['read', 'read-self', 'hist_sync', 'peer_msg', 'sender', 'inactive', 'played', undefined];
|
|
72
|
+
export type MessageReceiptType = (typeof MESSAGE_RECEIPT_TYPES)[number];
|
|
55
73
|
export type MediaConnInfo = {
|
|
56
74
|
auth: string;
|
|
57
75
|
ttl: number;
|
package/lib/Types/Message.js
CHANGED
|
@@ -7,4 +7,15 @@ export const WAMessageAddressingMode = Object.freeze({
|
|
|
7
7
|
PN: 'pn',
|
|
8
8
|
LID: 'lid'
|
|
9
9
|
});
|
|
10
|
+
/** `undefined` is a member: it is how upstream spells a delivery receipt. */
|
|
11
|
+
export const MESSAGE_RECEIPT_TYPES = [
|
|
12
|
+
'read',
|
|
13
|
+
'read-self',
|
|
14
|
+
'hist_sync',
|
|
15
|
+
'peer_msg',
|
|
16
|
+
'sender',
|
|
17
|
+
'inactive',
|
|
18
|
+
'played',
|
|
19
|
+
undefined
|
|
20
|
+
];
|
|
10
21
|
//# sourceMappingURL=Message.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse a value outside a closed set, naming the method, the parameter, what
|
|
3
|
+
* arrived and everything that is accepted.
|
|
4
|
+
*
|
|
5
|
+
* Call it in the synchronous prefix of the public method, ahead of the first
|
|
6
|
+
* `await`. That is what puts the caller's own frame in the stack: a
|
|
7
|
+
* fire-and-forget call leaves no awaiting frame for V8 to stitch an async
|
|
8
|
+
* stack onto, and past the bridge boundary the rejection is built inside wasm
|
|
9
|
+
* and carries nothing but wasm frames.
|
|
10
|
+
*
|
|
11
|
+
* The domain is always the values that define the parameter's type, never a
|
|
12
|
+
* second list written beside it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const assertArgumentDomain: <Value extends string | undefined>(method: string, parameter: string, value: unknown, domain: readonly Value[]) => Value;
|
|
15
|
+
//# sourceMappingURL=argument-domain.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Boom } from './boom.js';
|
|
2
|
+
/** Quoted when it is a string, bare otherwise, so `""` and `undefined` read apart. */
|
|
3
|
+
const shown = (value) => {
|
|
4
|
+
if (typeof value === 'string')
|
|
5
|
+
return JSON.stringify(value);
|
|
6
|
+
try {
|
|
7
|
+
return String(value);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// A null-prototype object, a revoked proxy, a trap that throws. `typeof`
|
|
11
|
+
// reads nothing off the value, so reporting it cannot fail on it.
|
|
12
|
+
return `[${typeof value}]`;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Refuse a value outside a closed set, naming the method, the parameter, what
|
|
17
|
+
* arrived and everything that is accepted.
|
|
18
|
+
*
|
|
19
|
+
* Call it in the synchronous prefix of the public method, ahead of the first
|
|
20
|
+
* `await`. That is what puts the caller's own frame in the stack: a
|
|
21
|
+
* fire-and-forget call leaves no awaiting frame for V8 to stitch an async
|
|
22
|
+
* stack onto, and past the bridge boundary the rejection is built inside wasm
|
|
23
|
+
* and carries nothing but wasm frames.
|
|
24
|
+
*
|
|
25
|
+
* The domain is always the values that define the parameter's type, never a
|
|
26
|
+
* second list written beside it.
|
|
27
|
+
*/
|
|
28
|
+
export const assertArgumentDomain = (method, parameter, value, domain) => {
|
|
29
|
+
if (domain.includes(value))
|
|
30
|
+
return value;
|
|
31
|
+
const error = new Boom(`${method}: ${JSON.stringify(parameter)} must be one of ${domain.map(shown).join(', ')}, received ${shown(value)}`, { statusCode: 400, data: { parameter, value, accepted: [...domain] } });
|
|
32
|
+
// Drop this frame: the method the consumer called is the useful top.
|
|
33
|
+
Error.captureStackTrace(error, assertArgumentDomain);
|
|
34
|
+
throw error;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=argument-domain.js.map
|
|
@@ -143,7 +143,14 @@ const append = (data, historyCache, event, eventData, logger) => {
|
|
|
143
143
|
case 'chats.upsert': {
|
|
144
144
|
for (const chat of eventData) {
|
|
145
145
|
const id = chat.id || '';
|
|
146
|
-
|
|
146
|
+
// The history set is only consulted for a chat that *has* an id.
|
|
147
|
+
// Upstream guards the lookup with `id &&`, and this port had dropped
|
|
148
|
+
// it: an id-less chat then folded into whatever id-less entry a
|
|
149
|
+
// buffered history set happened to carry, summing their unread counts,
|
|
150
|
+
// where upstream releases it as its own `chats.upsert`. Found by the
|
|
151
|
+
// buffer differential once history rows started drawing from the same
|
|
152
|
+
// identity pool as live traffic.
|
|
153
|
+
let existing = data.chatUpserts[id] || (id ? data.historySets.chats[id] : undefined);
|
|
147
154
|
if (existing)
|
|
148
155
|
concatChats(existing, chat);
|
|
149
156
|
else {
|
|
@@ -185,14 +192,37 @@ const append = (data, historyCache, event, eventData, logger) => {
|
|
|
185
192
|
case 'contacts.upsert': {
|
|
186
193
|
for (const contact of eventData) {
|
|
187
194
|
const existing = data.contactUpserts[contact.id] || data.historySets.contacts[contact.id];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
// A `contacts.update` already buffered for this id arrived *before*
|
|
196
|
+
// this upsert, so it is folded in first and the upsert's own values
|
|
197
|
+
// win where the two carry the same field. Folding it last — which is
|
|
198
|
+
// what this did — let a stale name overwrite the one that came after
|
|
199
|
+
// it, and the buffer released the older of the two. Fields only the
|
|
200
|
+
// update carried still survive, which upstream drops.
|
|
192
201
|
const pending = data.contactUpdates[contact.id];
|
|
193
|
-
if (pending)
|
|
194
|
-
Object.assign(existing || contact, pending);
|
|
202
|
+
if (pending)
|
|
195
203
|
delete data.contactUpdates[contact.id];
|
|
204
|
+
if (existing) {
|
|
205
|
+
if (pending)
|
|
206
|
+
Object.assign(existing, pending);
|
|
207
|
+
Object.assign(existing, trimUndefined(contact));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Filling the gaps in `contact` rather than merging onto `pending`
|
|
211
|
+
// and storing that: `pending` was grown from `{}` by the update
|
|
212
|
+
// branch, and accumulating into it — or into a fresh literal —
|
|
213
|
+
// measured about twice the cost of writing into the contact the
|
|
214
|
+
// caller handed us, which arrives with a settled shape. An
|
|
215
|
+
// explicitly-`undefined` field counts as absent, which is what
|
|
216
|
+
// `trimUndefined` would decide and costs no second pass.
|
|
217
|
+
if (pending) {
|
|
218
|
+
const target = contact;
|
|
219
|
+
const source = pending;
|
|
220
|
+
for (const field in source) {
|
|
221
|
+
if (target[field] === undefined)
|
|
222
|
+
target[field] = source[field];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
data.contactUpserts[contact.id] = contact;
|
|
196
226
|
}
|
|
197
227
|
}
|
|
198
228
|
break;
|
|
@@ -326,17 +356,21 @@ const consolidateEvents = (data) => {
|
|
|
326
356
|
if (Array.isArray(values) && values.length > 0)
|
|
327
357
|
events[event] = values;
|
|
328
358
|
};
|
|
359
|
+
// The order of these writes is the contract, not an implementation detail.
|
|
360
|
+
// A flush walks `Object.keys()` of this map, so insertion order decides both
|
|
361
|
+
// the key order a `process()` handler iterates and the order the individual
|
|
362
|
+
// events are re-dispatched to `.on()` listeners. Handlers that assume
|
|
363
|
+
// upstream's sequence — messages before the contacts they reference — see a
|
|
364
|
+
// different interleaving if these move. Keep them aligned with upstream's
|
|
365
|
+
// `consolidateEvents`.
|
|
329
366
|
assignArray('chats.upsert', Object.values(data.chatUpserts));
|
|
330
367
|
assignArray('chats.update', Object.values(data.chatUpdates));
|
|
331
368
|
assignArray('chats.delete', [...data.chatDeletes]);
|
|
332
|
-
assignArray('contacts.upsert', Object.values(data.contactUpserts));
|
|
333
|
-
assignArray('contacts.update', Object.values(data.contactUpdates));
|
|
334
|
-
assignArray('messages.update', Object.values(data.messageUpdates));
|
|
335
|
-
assignArray('groups.update', Object.values(data.groupUpdates));
|
|
336
369
|
const upserts = Object.values(data.messageUpserts);
|
|
337
370
|
if (upserts.length) {
|
|
338
371
|
events['messages.upsert'] = { messages: upserts.map(item => item.message), type: upserts[0].type };
|
|
339
372
|
}
|
|
373
|
+
assignArray('messages.update', Object.values(data.messageUpdates));
|
|
340
374
|
const deleted = Object.values(data.messageDeletes);
|
|
341
375
|
if (deleted.length)
|
|
342
376
|
events['messages.delete'] = { keys: deleted };
|
|
@@ -346,6 +380,9 @@ const consolidateEvents = (data) => {
|
|
|
346
380
|
const receipts = Object.values(data.messageReceipts).flatMap(({ key, userReceipt }) => userReceipt.map(receipt => ({ key, receipt })));
|
|
347
381
|
if (receipts.length)
|
|
348
382
|
events['message-receipt.update'] = receipts;
|
|
383
|
+
assignArray('contacts.upsert', Object.values(data.contactUpserts));
|
|
384
|
+
assignArray('contacts.update', Object.values(data.contactUpdates));
|
|
385
|
+
assignArray('groups.update', Object.values(data.groupUpdates));
|
|
349
386
|
return events;
|
|
350
387
|
};
|
|
351
388
|
/**
|
package/lib/Utils/messages.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export type DownloadMediaMessageContext = {
|
|
|
55
55
|
/** Bridge client for media download. Falls back to the registered one. */
|
|
56
56
|
waClient?: Pick<WasmWhatsAppClient, 'downloadMedia' | 'downloadMediaStream'>;
|
|
57
57
|
};
|
|
58
|
+
export declare const MEDIA_DOWNLOAD_TYPES: readonly ['buffer', 'stream'];
|
|
59
|
+
export type MediaDownloadType = (typeof MEDIA_DOWNLOAD_TYPES)[number];
|
|
58
60
|
/**
|
|
59
61
|
* Downloads the given message. Throws an error if it's not a media message.
|
|
60
62
|
*
|
|
@@ -68,7 +70,7 @@ export type DownloadMediaMessageContext = {
|
|
|
68
70
|
* several sockets should pass `ctx` explicitly, because the registration points
|
|
69
71
|
* at whichever client was created last.
|
|
70
72
|
*/
|
|
71
|
-
export declare const downloadMediaMessage: <Type extends
|
|
73
|
+
export declare const downloadMediaMessage: <Type extends MediaDownloadType>(message: WAMessage, type: Type, options: MediaDownloadOptions, ctx?: DownloadMediaMessageContext) => Promise<Type extends "buffer" ? Buffer<ArrayBufferLike> : Readable>;
|
|
72
74
|
export declare const _registerActiveBridgeClient: (client: WasmWhatsAppClient, logger?: ILogger) => void;
|
|
73
75
|
/**
|
|
74
76
|
* Drop the module-level pointer when `sock.end()` frees the client it points
|
package/lib/Utils/messages.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT
|
|
|
6
6
|
import { WAMessageStatus, WAProto } from '../Types/index.js';
|
|
7
7
|
import { proto } from '../WAProto/runtime.js';
|
|
8
8
|
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
|
+
import { assertArgumentDomain } from './argument-domain.js';
|
|
9
10
|
import { Boom } from './boom.js';
|
|
10
11
|
import { sha256 } from './crypto.js';
|
|
11
12
|
import { getKeyAuthor, toNumber, unixTimestampSeconds } from './generics.js';
|
|
@@ -227,22 +228,45 @@ export const generateForwardMessageContent = (message, forceForward) => {
|
|
|
227
228
|
throw new Boom('no content in message', { statusCode: 400 });
|
|
228
229
|
}
|
|
229
230
|
content = normalizeMessageContent(content);
|
|
230
|
-
// Shallow clone —
|
|
231
|
+
// Shallow clone of the outer map — one entry on it is rewritten below.
|
|
231
232
|
content = { ...content };
|
|
232
233
|
let key = Object.keys(content)[0];
|
|
233
234
|
let score = content?.[key]?.contextInfo?.forwardingScore || 0;
|
|
234
235
|
score += message.key.fromMe && !forceForward ? 0 : 1;
|
|
236
|
+
const contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
|
|
237
|
+
// The nested message object is the *caller's*, reached through the shallow
|
|
238
|
+
// clone above, so writing `contextInfo` onto it wrote into their argument.
|
|
239
|
+
// And it replaces rather than merges: a caller who forwarded a quoted message
|
|
240
|
+
// found `stanzaId` and `participant` gone from their own object afterwards.
|
|
241
|
+
// Upstream leaves the argument untouched.
|
|
242
|
+
//
|
|
243
|
+
// Rebuilt rather than deep-copied, and only the one object being written.
|
|
244
|
+
// Upstream's copy is `proto.Message.decode(proto.Message.encode(content))` —
|
|
245
|
+
// a full serialise/parse round trip on a hot send path — which is not worth
|
|
246
|
+
// paying to fix an aliasing bug.
|
|
235
247
|
if (key === 'conversation') {
|
|
236
|
-
|
|
248
|
+
// This object is created here, so nothing of the caller's is aliased and
|
|
249
|
+
// the `contextInfo` goes straight in. Same allocation count as before.
|
|
250
|
+
content.extendedTextMessage = { text: content[key], contextInfo };
|
|
237
251
|
delete content.conversation;
|
|
238
252
|
key = 'extendedTextMessage';
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
253
|
+
return content;
|
|
254
|
+
}
|
|
255
|
+
const nested = content[key];
|
|
256
|
+
// A plain object is the only thing worth copying, and the only thing that can
|
|
257
|
+
// be the caller's to damage. Anything else — an absent slot on an empty
|
|
258
|
+
// message, a primitive, an array — keeps the original assignment, which
|
|
259
|
+
// throws for exactly the inputs it threw for before and that upstream throws
|
|
260
|
+
// for too. Spreading those instead invented a property named `"undefined"`
|
|
261
|
+
// where upstream raised a TypeError.
|
|
262
|
+
if (typeof nested === 'object' && nested !== null && !Array.isArray(nested)) {
|
|
263
|
+
// One shallow spread, of exactly the object being modified. This is the
|
|
264
|
+
// whole cost of the fix, and it is unavoidable: not writing into the
|
|
265
|
+
// caller's object means writing into a different one.
|
|
266
|
+
content[key] = { ...nested, contextInfo };
|
|
243
267
|
}
|
|
244
268
|
else {
|
|
245
|
-
|
|
269
|
+
nested.contextInfo = contextInfo;
|
|
246
270
|
}
|
|
247
271
|
return content;
|
|
248
272
|
};
|
|
@@ -664,6 +688,7 @@ export const extractMessageContent = (content) => {
|
|
|
664
688
|
}
|
|
665
689
|
return content;
|
|
666
690
|
};
|
|
691
|
+
export const MEDIA_DOWNLOAD_TYPES = ['buffer', 'stream'];
|
|
667
692
|
/**
|
|
668
693
|
* Downloads the given message. Throws an error if it's not a media message.
|
|
669
694
|
*
|
|
@@ -678,6 +703,9 @@ export const extractMessageContent = (content) => {
|
|
|
678
703
|
* at whichever client was created last.
|
|
679
704
|
*/
|
|
680
705
|
export const downloadMediaMessage = async (message, type, options, ctx) => {
|
|
706
|
+
// Anything but 'buffer' used to take the stream branch, so a typo returned
|
|
707
|
+
// a Readable to a caller holding it as a Buffer.
|
|
708
|
+
assertArgumentDomain('downloadMediaMessage', 'type', type, MEDIA_DOWNLOAD_TYPES);
|
|
681
709
|
const waClient = ctx?.waClient ?? activeBridgeClient;
|
|
682
710
|
if (!waClient) {
|
|
683
711
|
throw new Boom('downloadMediaMessage: no bridge client available, and the download, its CDN failover and its decryption all happen in the engine. Pass `{ waClient: sock.waClient }`, use `sock.downloadMedia(message, type, options)`, or call after `makeWASocket()` has initialized.', { statusCode: 500 });
|
|
@@ -36,6 +36,15 @@ export declare const processHistoryMessage: (item: proto.IHistorySync, logger?:
|
|
|
36
36
|
export declare const downloadHistory: (msg: proto.Message.IHistorySyncNotification, options: RequestInit) => Promise<proto.HistorySync>;
|
|
37
37
|
/** Resolve inline or external history-sync content and normalize its public payload. */
|
|
38
38
|
export declare const downloadAndProcessHistorySyncNotification: (msg: proto.Message.IHistorySyncNotification, options: RequestInit, logger?: ILogger) => Promise<ProcessedHistorySync>;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Extract a history-sync notification through the same wrapper normalization as
|
|
41
|
+
* upstream.
|
|
42
|
+
*
|
|
43
|
+
* Returns `undefined` when the message carries none. It used to throw a Boom
|
|
44
|
+
* 400, which breaks the shape every caller writes against a drop-in API —
|
|
45
|
+
* `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning.
|
|
46
|
+
* "Absent" is the ordinary case here, not an error: any message that is not a
|
|
47
|
+
* history sync takes this path.
|
|
48
|
+
*/
|
|
49
|
+
export declare const getHistoryMsg: (message: proto.IMessage) => proto.Message.IHistorySyncNotification | undefined;
|
|
41
50
|
//# sourceMappingURL=process-history-message.d.ts.map
|
|
@@ -14,7 +14,6 @@ import { inflateZlib } from '@oxidezap/whatsapp-rust-bridge';
|
|
|
14
14
|
import { proto } from '../WAProto/runtime.js';
|
|
15
15
|
import { WAProto } from '../Types/index.js';
|
|
16
16
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/jid-utils.js';
|
|
17
|
-
import { Boom } from './boom.js';
|
|
18
17
|
import { toNumber } from './generics.js';
|
|
19
18
|
import { downloadContentFromMessage, normalizeMessageContent } from './messages.js';
|
|
20
19
|
import { createSparseArray } from './sparse-array.js';
|
|
@@ -199,13 +198,18 @@ export const downloadAndProcessHistorySyncNotification = async (msg, options, lo
|
|
|
199
198
|
: await downloadHistory(msg, options);
|
|
200
199
|
return processHistoryMessage(historyMsg, logger);
|
|
201
200
|
};
|
|
202
|
-
/**
|
|
201
|
+
/**
|
|
202
|
+
* Extract a history-sync notification through the same wrapper normalization as
|
|
203
|
+
* upstream.
|
|
204
|
+
*
|
|
205
|
+
* Returns `undefined` when the message carries none. It used to throw a Boom
|
|
206
|
+
* 400, which breaks the shape every caller writes against a drop-in API —
|
|
207
|
+
* `const h = getHistoryMsg(msg); if (!h) return` crashed instead of returning.
|
|
208
|
+
* "Absent" is the ordinary case here, not an error: any message that is not a
|
|
209
|
+
* history sync takes this path.
|
|
210
|
+
*/
|
|
203
211
|
export const getHistoryMsg = (message) => {
|
|
204
212
|
const normalizedContent = message ? normalizeMessageContent(message) : undefined;
|
|
205
|
-
|
|
206
|
-
if (!historySyncNotification) {
|
|
207
|
-
throw new Boom('Message does not contain a history sync notification', { statusCode: 400 });
|
|
208
|
-
}
|
|
209
|
-
return historySyncNotification;
|
|
213
|
+
return normalizedContent?.protocolMessage?.historySyncNotification ?? undefined;
|
|
210
214
|
};
|
|
211
215
|
//# sourceMappingURL=process-history-message.js.map
|
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { createDeviceProjection } from '../Compatibility/legacy-store/device.js';
|
|
2
3
|
import { projectNativeStore } from '../Compatibility/legacy-store/native-projection.js';
|
|
3
4
|
import { initAuthCreds } from './generics.js';
|
|
4
5
|
import { useBridgeStore } from './use-bridge-store.js';
|
|
6
|
+
/**
|
|
7
|
+
* The store namespace and keys the engine writes its own device under.
|
|
8
|
+
*
|
|
9
|
+
* `<folder>/device-device.bin` and `<folder>/device-account.bin` on disk.
|
|
10
|
+
*/
|
|
11
|
+
const DEVICE_STORE = 'device';
|
|
12
|
+
const DEVICE_RECORDS = ['device', 'account'];
|
|
13
|
+
/**
|
|
14
|
+
* Rebuild the credential mirror from the device the engine persisted.
|
|
15
|
+
*
|
|
16
|
+
* Without this the mirror is whatever `initAuthCreds()` just made up: every
|
|
17
|
+
* restart handed back `registered: false` and `me: undefined` for a session
|
|
18
|
+
* that was paired and working, because nothing on this path ever read the
|
|
19
|
+
* device back. The hydration itself already existed for `wrapLegacyStore`
|
|
20
|
+
* (`Compatibility/legacy-store/adapter.ts`), which only runs for callers that
|
|
21
|
+
* bring their own `{ creds, keys }`; a caller using this function goes straight
|
|
22
|
+
* to the bridge store and used to skip it entirely.
|
|
23
|
+
*
|
|
24
|
+
* A record that fails to decode is skipped rather than fatal: a mirror missing
|
|
25
|
+
* a field is worth less than a socket that will not start, and the engine reads
|
|
26
|
+
* its own device from the same bytes regardless of what this makes of them.
|
|
27
|
+
*/
|
|
28
|
+
const hydrateFromStore = async (store, creds) => {
|
|
29
|
+
const projection = createDeviceProjection(creds);
|
|
30
|
+
for (const record of DEVICE_RECORDS) {
|
|
31
|
+
const payload = await store.get(DEVICE_STORE, record);
|
|
32
|
+
if (!payload)
|
|
33
|
+
continue;
|
|
34
|
+
try {
|
|
35
|
+
projection.prepare(record, payload)();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* a record we cannot read leaves that part of the mirror at its default */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
5
42
|
/**
|
|
6
43
|
* Creates a file-based authentication state for the Rust bridge.
|
|
7
44
|
*
|
|
@@ -30,6 +67,14 @@ export const useMultiFileAuthState = async (folder) => {
|
|
|
30
67
|
}
|
|
31
68
|
const store = await useBridgeStore(folder);
|
|
32
69
|
const creds = initAuthCreds();
|
|
70
|
+
// Before `projectNativeStore`, and that ordering is the whole point: the
|
|
71
|
+
// projection captures `signedIdentityKey.public` and `registrationId` off
|
|
72
|
+
// `creds` when it is called, and builds the Signal codecs around them.
|
|
73
|
+
// Hydrating afterwards would leave those codecs holding the identity of the
|
|
74
|
+
// throwaway `initAuthCreds()` rather than the device's, so a legacy session
|
|
75
|
+
// written through this store would be imported under the wrong local
|
|
76
|
+
// identity.
|
|
77
|
+
await hydrateFromStore(store, creds);
|
|
33
78
|
const keys = projectNativeStore(store, creds);
|
|
34
79
|
return {
|
|
35
80
|
state: { creds, keys, store },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxidezap/baileyrs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"lib/**/*",
|
|
48
48
|
"!lib/**/*.map",
|
|
49
49
|
"!lib/**/__tests__/**",
|
|
50
|
+
"!lib/__fuzz__/**",
|
|
50
51
|
"!lib/**/*.test.*",
|
|
51
52
|
"!lib/**/*.test-e2e.*"
|
|
52
53
|
],
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"compat:audit:missing": "npm run build --silent && node scripts/compatibility/audit.ts --only-missing",
|
|
59
60
|
"compat:audit:proto": "node scripts/compatibility/proto-runtime-audit.ts --details --strict",
|
|
60
61
|
"compat:audit:wire": "node scripts/compatibility/wire-fidelity-audit.ts --details --strict",
|
|
62
|
+
"compat:audit:lifecycle": "node scripts/compatibility/lifecycle-contract-audit.ts --strict",
|
|
61
63
|
"compat:audit:strict": "npm run build --silent && node scripts/compatibility/audit.ts --strict --only-missing",
|
|
62
64
|
"compat:sync-waproto": "node scripts/compatibility/waproto-facade.ts --sync",
|
|
63
65
|
"compat:check-waproto": "node scripts/compatibility/waproto-facade.ts --check",
|
|
@@ -70,13 +72,17 @@
|
|
|
70
72
|
"prepack": "npm run build && node scripts/check-pack.ts",
|
|
71
73
|
"prepare": "npm run build",
|
|
72
74
|
"test": "node --test",
|
|
75
|
+
"fuzz": "node --test --test-timeout=600000 ./src/__fuzz__/**/*.test.ts",
|
|
76
|
+
"fuzz:deep": "FUZZ_MODE=deep node --expose-gc --test --test-timeout=1800000 ./src/__fuzz__/**/*.test.ts",
|
|
77
|
+
"fuzz:record": "FUZZ_RECORD=1 node --test --test-timeout=600000 ./src/__fuzz__/**/*.test.ts",
|
|
78
|
+
"fuzz:report": "node scripts/fuzz/report.ts",
|
|
73
79
|
"test:compat-auditor": "node --test scripts/compatibility/__tests__/audit.test.ts",
|
|
74
80
|
"typecheck:compat-auditor": "npm run build --silent && npm run compat:check-waproto --silent && npm run compat:layers --silent && tsc -p scripts/compatibility/tsconfig.json",
|
|
75
|
-
"test:e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 ADV_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= node --expose-gc --test --test-concurrency=1 ./src/__tests__/e2e/*.test-e2e.ts"
|
|
81
|
+
"test:e2e": "NODE_TLS_REJECT_UNAUTHORIZED=0 ADV_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= node --expose-gc --test --test-concurrency=1 ./src/__tests__/e2e/*.test-e2e.ts ./scripts/compatibility/__tests__/*.test-e2e.ts"
|
|
76
82
|
},
|
|
77
83
|
"dependencies": {
|
|
78
84
|
"@hapi/boom": "^9.1.4",
|
|
79
|
-
"@oxidezap/whatsapp-rust-bridge": "0.
|
|
85
|
+
"@oxidezap/whatsapp-rust-bridge": "0.11.0",
|
|
80
86
|
"long": "^5.3.2",
|
|
81
87
|
"pino": "^10.3.1",
|
|
82
88
|
"protobufjs": "^7.6.5"
|