@oxidezap/baileyrs 0.2.7 → 0.2.9
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 +23 -0
- package/lib/Compatibility/legacy-store/codecs/basic.js +10 -3
- package/lib/Compatibility/legacy-store/codecs/signal.js +21 -9
- package/lib/Compatibility/legacy-store/common.js +31 -6
- package/lib/Compatibility/legacy-store/constants.d.ts +0 -1
- package/lib/Compatibility/legacy-store/constants.js +0 -1
- package/lib/Socket/events.js +11 -1
- package/lib/Types/Message.d.ts +2 -0
- package/lib/Utils/messages.js +94 -4
- package/lib/Utils/process-message.js +38 -4
- package/lib/WABinary/jid-utils.js +62 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -374,6 +374,29 @@ baileyrs auto-wraps, including `useLegacyMultiFileAuthState`. It does not apply
|
|
|
374
374
|
to `useMultiFileAuthState`, whose `keys` is a projection over the engine's own
|
|
375
375
|
store; the `bridge-` namespaces never surface through it.
|
|
376
376
|
|
|
377
|
+
The move back down is not symmetric either. From 0.2.8 on, the core buffers a
|
|
378
|
+
skipped-message key as its seed alone, where an earlier release wrote the
|
|
379
|
+
derived cipher/mac/iv triple beside it. Forward is fine — a record written
|
|
380
|
+
before this carries both, and the newer engine reads it unchanged. Backward is
|
|
381
|
+
not: an older engine cannot parse a record whose skipped keys carry no triple,
|
|
382
|
+
and it fails on **the whole record**, not on the one key.
|
|
383
|
+
|
|
384
|
+
Where that bites depends on which bytes the engine is handed, and a wrapped
|
|
385
|
+
store is not automatically on the safe side of it. It holds the Baileys
|
|
386
|
+
projection, which has only ever held seeds — but it also mirrors the core's
|
|
387
|
+
exact bytes under `bridge-native-session`, and a read prefers that mirror for as
|
|
388
|
+
long as its fingerprint still matches the projection. A downgrade can therefore
|
|
389
|
+
be handed the newer record with a perfectly readable projection sitting right
|
|
390
|
+
beside it.
|
|
391
|
+
|
|
392
|
+
Dropping a mirror row makes the next read rebuild it from the projection, which
|
|
393
|
+
is the format both versions agree on — but only for a session that has one. A
|
|
394
|
+
session that turned native-only lives in that row alone, so dropping *that* one
|
|
395
|
+
loses the session outright, which is the case the warning above is about. A
|
|
396
|
+
session with no skipped keys buffered at the time is unaffected either way: the
|
|
397
|
+
difference only exists while a chain is holding keys for messages that arrived
|
|
398
|
+
out of order.
|
|
399
|
+
|
|
377
400
|
## Disclaimer
|
|
378
401
|
|
|
379
402
|
This project is not affiliated with, endorsed by, or in any way officially connected to
|
|
@@ -102,11 +102,18 @@ function tcTokenCodec() {
|
|
|
102
102
|
},
|
|
103
103
|
toLegacy(_key, value) {
|
|
104
104
|
const native = fromJsonBytes(value);
|
|
105
|
-
|
|
105
|
+
// Added rather than set to `undefined`, matching upstream, which
|
|
106
|
+
// only carries the field when it has a value. An undefined-valued
|
|
107
|
+
// key survives in memory but not through a store, and the mirror
|
|
108
|
+
// fingerprint is taken on both sides of that boundary — see the
|
|
109
|
+
// note in `canonicalize`.
|
|
110
|
+
const legacy = {
|
|
106
111
|
token: Buffer.from(requireByteArray(native.token, 'native TC token')),
|
|
107
|
-
timestamp: (native.token_timestamp ?? TimeValue.UNKNOWN_SECONDS).toString()
|
|
108
|
-
senderTimestamp: native.sender_timestamp == null ? undefined : native.sender_timestamp.toString()
|
|
112
|
+
timestamp: (native.token_timestamp ?? TimeValue.UNKNOWN_SECONDS).toString()
|
|
109
113
|
};
|
|
114
|
+
if (native.sender_timestamp != null)
|
|
115
|
+
legacy.senderTimestamp = native.sender_timestamp.toString();
|
|
116
|
+
return legacy;
|
|
110
117
|
}
|
|
111
118
|
};
|
|
112
119
|
}
|
|
@@ -177,7 +177,16 @@ function coreEntryToLegacy(session) {
|
|
|
177
177
|
messageKeys: messageKeysToLegacy(chain.messageKeys)
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
-
|
|
180
|
+
// Both optional parts are *added* rather than set to `undefined`, which is
|
|
181
|
+
// what upstream does (`session_record.js` guards `if (this.pendingPreKey)`,
|
|
182
|
+
// `session_builder.js` guards `if (device.preKey)`). The distinction is
|
|
183
|
+
// invisible once the value reaches a store — JSON drops an undefined-valued
|
|
184
|
+
// key — but it is visible to the mirror fingerprint, which is taken from
|
|
185
|
+
// the live object on write and from the reloaded one on read. Emitting the
|
|
186
|
+
// key made those two disagree for every session without a pending pre-key,
|
|
187
|
+
// so the mirror was rejected on the next read and the record rebuilt from
|
|
188
|
+
// the projection, discarding the counter lease the rebuild cannot express.
|
|
189
|
+
const entry = {
|
|
181
190
|
registrationId: session.registrationId,
|
|
182
191
|
currentRatchet: {
|
|
183
192
|
ephemeralKeyPair: {
|
|
@@ -196,15 +205,18 @@ function coreEntryToLegacy(session) {
|
|
|
196
205
|
created: session.index.createdAtMs,
|
|
197
206
|
remoteIdentityKey: toBase64(session.index.remoteIdentityKey)
|
|
198
207
|
},
|
|
199
|
-
_chains: chains
|
|
200
|
-
pendingPreKey: session.pendingPreKey
|
|
201
|
-
? {
|
|
202
|
-
preKeyId: session.pendingPreKey.preKeyId,
|
|
203
|
-
signedKeyId: session.pendingPreKey.signedPreKeyId,
|
|
204
|
-
baseKey: toBase64(session.pendingPreKey.baseKey)
|
|
205
|
-
}
|
|
206
|
-
: undefined
|
|
208
|
+
_chains: chains
|
|
207
209
|
};
|
|
210
|
+
if (session.pendingPreKey) {
|
|
211
|
+
entry.pendingPreKey = {
|
|
212
|
+
signedKeyId: session.pendingPreKey.signedPreKeyId,
|
|
213
|
+
baseKey: toBase64(session.pendingPreKey.baseKey)
|
|
214
|
+
};
|
|
215
|
+
if (session.pendingPreKey.preKeyId !== undefined) {
|
|
216
|
+
entry.pendingPreKey.preKeyId = session.pendingPreKey.preKeyId;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return entry;
|
|
208
220
|
}
|
|
209
221
|
function sessionToLegacy(nativeBytes) {
|
|
210
222
|
const projection = projectLegacySessionRecordV1(nativeBytes);
|
|
@@ -16,15 +16,29 @@ export const toBytes = (value) => {
|
|
|
16
16
|
return null;
|
|
17
17
|
};
|
|
18
18
|
export const bytesToNumbers = (value) => Array.from(value);
|
|
19
|
+
/**
|
|
20
|
+
* Marks a value JSON would not have kept, so the object and array branches can
|
|
21
|
+
* each drop it the way `JSON.stringify` does. A plain `undefined` return could
|
|
22
|
+
* not be told apart from a property that is genuinely absent.
|
|
23
|
+
*/
|
|
24
|
+
const ABSENT = Symbol('absent');
|
|
19
25
|
/**
|
|
20
26
|
* Stable, runtime-independent representation used only to notice legacy-side
|
|
21
27
|
* ownership changes. Buffer and Uint8Array intentionally share one shape.
|
|
28
|
+
*
|
|
29
|
+
* `undefined` follows `JSON.stringify`, and that is deliberately asymmetric: an
|
|
30
|
+
* object property is dropped, an array element becomes `null`. The fingerprint
|
|
31
|
+
* is taken from the live projection when the mirror is written and from the
|
|
32
|
+
* reloaded one when it is read, so any distinction a store cannot carry makes
|
|
33
|
+
* the two disagree forever. Tagging `undefined` as its own value was such a
|
|
34
|
+
* distinction — no persisted store can express it, and the mirror it invalidated
|
|
35
|
+
* is the only copy of the record fields the projection has no room for.
|
|
22
36
|
*/
|
|
23
37
|
function canonicalize(value, seen) {
|
|
24
38
|
if (value === null)
|
|
25
39
|
return null;
|
|
26
40
|
if (value === undefined)
|
|
27
|
-
return
|
|
41
|
+
return ABSENT;
|
|
28
42
|
if (typeof value === 'string' || typeof value === 'boolean')
|
|
29
43
|
return value;
|
|
30
44
|
if (typeof value === 'number') {
|
|
@@ -51,11 +65,17 @@ function canonicalize(value, seen) {
|
|
|
51
65
|
throw new TypeError('cannot fingerprint a cyclic legacy store value');
|
|
52
66
|
seen.add(value);
|
|
53
67
|
try {
|
|
54
|
-
if (Array.isArray(value))
|
|
55
|
-
return value.map(item =>
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map(item => {
|
|
70
|
+
const canonical = canonicalize(item, seen);
|
|
71
|
+
return canonical === ABSENT ? null : canonical;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
56
74
|
const out = {};
|
|
57
75
|
for (const key of Object.keys(value).toSorted()) {
|
|
58
|
-
|
|
76
|
+
const canonical = canonicalize(value[key], seen);
|
|
77
|
+
if (canonical !== ABSENT)
|
|
78
|
+
out[key] = canonical;
|
|
59
79
|
}
|
|
60
80
|
return out;
|
|
61
81
|
}
|
|
@@ -64,8 +84,13 @@ function canonicalize(value, seen) {
|
|
|
64
84
|
}
|
|
65
85
|
}
|
|
66
86
|
function fingerprintLegacy(value) {
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
// A top-level absent value hashes as `null`: a store reports a row it does
|
|
88
|
+
// not hold as either, and the two have to agree for the same reason the
|
|
89
|
+
// property case does.
|
|
90
|
+
const canonical = canonicalize(value, new Set());
|
|
91
|
+
return createHash(MirrorEnvelope.HASH)
|
|
92
|
+
.update(JSON.stringify(canonical === ABSENT ? null : canonical))
|
|
93
|
+
.digest();
|
|
69
94
|
}
|
|
70
95
|
/** Persist exact native bytes together with the projection generation. */
|
|
71
96
|
export function encodeNativeEnvelope(payload, legacyValue) {
|
|
@@ -101,7 +101,6 @@ export declare const CanonicalTag: Readonly<{
|
|
|
101
101
|
readonly BYTES: '$bytes';
|
|
102
102
|
readonly DATE: '$date';
|
|
103
103
|
readonly NUMBER: '$number';
|
|
104
|
-
readonly UNDEFINED: '$undefined';
|
|
105
104
|
readonly UNSUPPORTED: '$unsupported';
|
|
106
105
|
}>;
|
|
107
106
|
export declare const NumericEncoding: Readonly<{
|
package/lib/Socket/events.js
CHANGED
|
@@ -35,7 +35,17 @@ const emitCBEvents = (ctx, node) => {
|
|
|
35
35
|
const id = l1.id;
|
|
36
36
|
if (id)
|
|
37
37
|
ws.emit(`${DEF_TAG_PREFIX}${id}`, node);
|
|
38
|
-
for
|
|
38
|
+
// `for..in` over the attrs rather than `Object.entries`: this runs for every
|
|
39
|
+
// stanza the socket sees and the pair array it built was thrown away
|
|
40
|
+
// immediately. `Object.entries` was own-only by construction though, and
|
|
41
|
+
// `BinaryNode` is public, so the walk is guarded rather than trusting every
|
|
42
|
+
// caller to hand over an attrs map with a bare prototype. An inherited
|
|
43
|
+
// enumerable would otherwise fire CB events for an attr the stanza does not
|
|
44
|
+
// carry; the check costs nothing next to the emits below.
|
|
45
|
+
for (const key in l1) {
|
|
46
|
+
if (!Object.hasOwn(l1, key))
|
|
47
|
+
continue;
|
|
48
|
+
const val = l1[key];
|
|
39
49
|
if (l2)
|
|
40
50
|
ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val},${l2}`, node);
|
|
41
51
|
ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val}`, node);
|
package/lib/Types/Message.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ export type PollMessageOptions = {
|
|
|
112
112
|
name: string;
|
|
113
113
|
selectableCount?: number;
|
|
114
114
|
values: string[];
|
|
115
|
+
/** 32 byte message secret to encrypt poll selections */
|
|
116
|
+
messageSecret?: Uint8Array;
|
|
115
117
|
toAnnouncementGroup?: boolean;
|
|
116
118
|
};
|
|
117
119
|
export type EventMessageOptions = {
|
package/lib/Utils/messages.js
CHANGED
|
@@ -8,6 +8,7 @@ import { proto } from '../WAProto/runtime.js';
|
|
|
8
8
|
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
|
|
9
9
|
import { assertArgumentDomain } from './argument-domain.js';
|
|
10
10
|
import { Boom } from './boom.js';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
11
12
|
import { sha256 } from './crypto.js';
|
|
12
13
|
import { getKeyAuthor, toNumber, unixTimestampSeconds } from './generics.js';
|
|
13
14
|
import { generateThumbnail, getAudioDuration, getAudioWaveform, getStream, toBuffer } from './messages-media.js';
|
|
@@ -434,6 +435,31 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
434
435
|
m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
|
|
435
436
|
m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
|
|
436
437
|
m.eventMessage.location = message.event.location;
|
|
438
|
+
// The sender's copy of the key that encrypts responses to this event;
|
|
439
|
+
// `decryptEventResponse` takes it as `eventEncKey`. WhatsApp Web writes it
|
|
440
|
+
// on the sender at creation (`WAWebSendEventCreationMsgAction`) and refuses
|
|
441
|
+
// to build an event edit without it (`WAWebCreateEncryptedEventEditMsgData`
|
|
442
|
+
// throws `EventCreationValidationError`).
|
|
443
|
+
//
|
|
444
|
+
// The core settles `messageSecret` on relay for messages that reach it
|
|
445
|
+
// without one — see the comment in `Socket/messages.ts` — but it settles it
|
|
446
|
+
// on the wire, where the caller never sees it. That is right for the
|
|
447
|
+
// reporting token and wrong for a key the caller has to keep.
|
|
448
|
+
//
|
|
449
|
+
// Written after `m.eventMessage`, for the reason spelled out on the poll
|
|
450
|
+
// branch below: the blocks that attach `contextInfo` take `Object.keys(m)[0]`
|
|
451
|
+
// as the content key. `event` is not `Mentionable` or `Contextable` today,
|
|
452
|
+
// so this is ordering that stays correct rather than ordering that is
|
|
453
|
+
// currently load-bearing — but it costs nothing and the poll branch shows
|
|
454
|
+
// what getting it wrong looks like.
|
|
455
|
+
//
|
|
456
|
+
// Spread rather than upstream's wholesale assignment: nothing else writes
|
|
457
|
+
// `messageContextInfo` on this branch today, so the two cannot differ, and
|
|
458
|
+
// this cannot silently drop a field a later change puts there.
|
|
459
|
+
m.messageContextInfo = {
|
|
460
|
+
...m.messageContextInfo,
|
|
461
|
+
messageSecret: message.event.messageSecret || randomBytes(32)
|
|
462
|
+
};
|
|
437
463
|
}
|
|
438
464
|
else if (hasNonNullishProperty(message, 'poll')) {
|
|
439
465
|
(_a = message.poll).selectableCount || (_a.selectableCount = 0);
|
|
@@ -465,6 +491,29 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
465
491
|
m.pollCreationMessage = pollCreationMessage;
|
|
466
492
|
}
|
|
467
493
|
}
|
|
494
|
+
// The key that encrypts the votes. `decryptPollVote` takes it as
|
|
495
|
+
// `pollEncKey`, and without it here the poll goes out fine but its own
|
|
496
|
+
// sender can never read the votes: the copy they store — the message
|
|
497
|
+
// `sendMessage` returns and emits as `messages.upsert` — is this object,
|
|
498
|
+
// and only `key.id` is patched from the relay result. WhatsApp Web writes it
|
|
499
|
+
// on the sender at creation too (`WAWebPollsSendPollCreationMsgAction`) and
|
|
500
|
+
// reuses the stored one for poll edits
|
|
501
|
+
// (`WAWebPollsGeneratePollEditMessageProto`).
|
|
502
|
+
//
|
|
503
|
+
// After the payload, not before, and that ordering is load-bearing: the
|
|
504
|
+
// `mentions` and `contextInfo` blocks below pick their target with
|
|
505
|
+
// `Object.keys(m)[0]`, so a `messageContextInfo` written first becomes the
|
|
506
|
+
// content key they attach to. `contextInfo` is not a field of
|
|
507
|
+
// `IMessageContextInfo`, so the encoder drops it and a poll sent with
|
|
508
|
+
// mentions loses them silently. `poll` is `Mentionable & Contextable`, so
|
|
509
|
+
// that is a combination callers can and do write. Upstream assigns it
|
|
510
|
+
// before the payload and has exactly that bug; measured on
|
|
511
|
+
// `{poll, mentions:['x@s.whatsapp.net']}`, its output carries no
|
|
512
|
+
// `mentionedJid` at all.
|
|
513
|
+
m.messageContextInfo = {
|
|
514
|
+
...m.messageContextInfo,
|
|
515
|
+
messageSecret: message.poll.messageSecret || randomBytes(32)
|
|
516
|
+
};
|
|
468
517
|
}
|
|
469
518
|
else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
|
|
470
519
|
m.protocolMessage = {
|
|
@@ -612,13 +661,54 @@ export const generateWAMessage = async (jid, content, options) => {
|
|
|
612
661
|
: options;
|
|
613
662
|
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, contentOptions), options);
|
|
614
663
|
};
|
|
664
|
+
/**
|
|
665
|
+
* The content keys that carry almost every message on a live socket, settled by
|
|
666
|
+
* identity before the structural predicate runs. Every name here already
|
|
667
|
+
* satisfies that predicate, so this only shortcuts the substring scan; it never
|
|
668
|
+
* changes which key wins.
|
|
669
|
+
*/
|
|
670
|
+
const isCommonContentKey = (key) => {
|
|
671
|
+
switch (key) {
|
|
672
|
+
case 'conversation':
|
|
673
|
+
case 'extendedTextMessage':
|
|
674
|
+
case 'imageMessage':
|
|
675
|
+
case 'videoMessage':
|
|
676
|
+
case 'audioMessage':
|
|
677
|
+
case 'stickerMessage':
|
|
678
|
+
case 'documentMessage':
|
|
679
|
+
case 'reactionMessage':
|
|
680
|
+
case 'protocolMessage':
|
|
681
|
+
case 'pollCreationMessage':
|
|
682
|
+
return true;
|
|
683
|
+
default:
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
};
|
|
615
687
|
/** Get the key to access the true type of content */
|
|
616
688
|
export const getContentType = (content) => {
|
|
617
|
-
if (content) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
689
|
+
if (!content) {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
// The answer stays positional (the first own key naming content wins), but
|
|
693
|
+
// the scan is an indexed loop with an early return instead of `Array.find`,
|
|
694
|
+
// so no closure is allocated and the common keys never reach `includes`.
|
|
695
|
+
//
|
|
696
|
+
// Deliberately still `Object.keys` and not `for..in`: a decoded
|
|
697
|
+
// `WAProto.Message` inherits a default for every field of the schema, so
|
|
698
|
+
// `for..in` would enumerate ~100 prototype keys per call (and would have to
|
|
699
|
+
// filter them back out with `hasOwn` to stay positional) where `Object.keys`
|
|
700
|
+
// yields only the handful that were actually set.
|
|
701
|
+
const keys = Object.keys(content);
|
|
702
|
+
for (let i = 0; i < keys.length; i++) {
|
|
703
|
+
const key = keys[i];
|
|
704
|
+
if (isCommonContentKey(key)) {
|
|
705
|
+
return key;
|
|
706
|
+
}
|
|
707
|
+
if (key.includes('Message') && key !== 'senderKeyDistributionMessage') {
|
|
708
|
+
return key;
|
|
709
|
+
}
|
|
621
710
|
}
|
|
711
|
+
return undefined;
|
|
622
712
|
};
|
|
623
713
|
const getFutureProofMessage = (message) => message?.ephemeralMessage ||
|
|
624
714
|
message?.viewOnceMessage ||
|
|
@@ -24,19 +24,53 @@ export const isRealMessage = (message) => {
|
|
|
24
24
|
!normalizedContent?.pollUpdateMessage);
|
|
25
25
|
};
|
|
26
26
|
export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !message.messageStubType;
|
|
27
|
+
/**
|
|
28
|
+
* WhatsApp Web's rules, which upstream Baileys does not always follow.
|
|
29
|
+
*
|
|
30
|
+
* **A field with nothing to normalise stays absent.** `WAWebMsgKey` only assigns
|
|
31
|
+
* the participant it was given — `h !== void 0 && (this.participant = h)` — and
|
|
32
|
+
* builds the message's identity by joining the parts that are present, so an
|
|
33
|
+
* empty string there would change what the key serialises to. Upstream runs
|
|
34
|
+
* every field through `jidNormalizedUser`, which answers `''` for a jid it was
|
|
35
|
+
* not given, and so writes `participant: ''` onto every direct-message key.
|
|
36
|
+
* Matching that would mean copying a bug into the key handed back to callers.
|
|
37
|
+
*
|
|
38
|
+
* **A hosted jid is re-encoded onto its plain server**, and the fallback that
|
|
39
|
+
* used to keep `@hosted` when the user part was empty is gone. WA Web accepts
|
|
40
|
+
* hosted only as `<digits>:99@hosted` (`WAWebWidValidator`), and every jid that
|
|
41
|
+
* reached the old fallback is one it rejects outright — so there is no
|
|
42
|
+
* behaviour to preserve there, and agreeing with upstream costs nothing. Valid
|
|
43
|
+
* hosted jids already agreed.
|
|
44
|
+
*/
|
|
27
45
|
const normalizeMessageJid = (jid) => {
|
|
28
46
|
if (!jid)
|
|
29
47
|
return undefined;
|
|
30
48
|
const hostedPn = isHostedPnUser(jid);
|
|
31
49
|
if (!hostedPn && !isHostedLidUser(jid))
|
|
32
50
|
return jidNormalizedUser(jid);
|
|
33
|
-
|
|
34
|
-
return user ? jidEncode(user, hostedPn ? 's.whatsapp.net' : 'lid') : jidNormalizedUser(jid);
|
|
51
|
+
return jidEncode(jidDecode(jid)?.user ?? null, hostedPn ? 's.whatsapp.net' : 'lid');
|
|
35
52
|
};
|
|
36
53
|
/** Normalize device/hosted JIDs and nested reaction/poll keys in place. */
|
|
37
54
|
export const cleanMessage = (message, meId, meLid) => {
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
// Normalise the fields that are there; do not add the ones that are not.
|
|
56
|
+
//
|
|
57
|
+
// Writing the result unconditionally put the property on the key even when
|
|
58
|
+
// there was nothing to normalise, so a direct-message key — which carries no
|
|
59
|
+
// participant — still gained one holding `undefined`, and `Object.keys` and a
|
|
60
|
+
// spread saw it. `WAWebMsgKey` assigns the participant it was given and
|
|
61
|
+
// leaves the key without one otherwise, and that absence is load-bearing
|
|
62
|
+
// there: it joins the parts that are present into the id the message is
|
|
63
|
+
// stored under.
|
|
64
|
+
//
|
|
65
|
+
// A field the caller did provide is normalised even when it comes back with
|
|
66
|
+
// nothing, which is the empty string upstream writes — they provided it, so
|
|
67
|
+
// it is theirs to have normalised, and leaving a `null` in place would only
|
|
68
|
+
// swap one falsy spelling for another.
|
|
69
|
+
for (const field of ['remoteJid', 'participant']) {
|
|
70
|
+
if (!Object.hasOwn(message.key, field) && message.key[field] == null)
|
|
71
|
+
continue;
|
|
72
|
+
message.key[field] = normalizeMessageJid(message.key[field]) ?? '';
|
|
73
|
+
}
|
|
40
74
|
const content = normalizeMessageContent(message.message);
|
|
41
75
|
if (content?.reactionMessage)
|
|
42
76
|
normaliseKey(content.reactionMessage.key);
|
|
@@ -57,8 +57,52 @@ export const jidDecode = (jid) => {
|
|
|
57
57
|
device: device ? +device : undefined
|
|
58
58
|
};
|
|
59
59
|
};
|
|
60
|
+
const CHAR_DEVICE_SEP = 58; // ':'
|
|
61
|
+
const CHAR_AGENT_SEP = 95; // '_'
|
|
62
|
+
/**
|
|
63
|
+
* Index at which the user component ends, given the index of the '@'.
|
|
64
|
+
*
|
|
65
|
+
* `jidDecode` derives the user by splitting the pre-`@` half on ':' and then on
|
|
66
|
+
* '_', so the user always runs to whichever of ':' or '_' comes first, and to
|
|
67
|
+
* the '@' when neither is there. Locating that boundary answers both callers
|
|
68
|
+
* below without the intermediate arrays and result object `jidDecode` allocates.
|
|
69
|
+
*/
|
|
70
|
+
const userEndBefore = (jid, sepIdx) => {
|
|
71
|
+
for (let i = 0; i < sepIdx; i++) {
|
|
72
|
+
const code = jid.charCodeAt(i);
|
|
73
|
+
if (code === CHAR_DEVICE_SEP || code === CHAR_AGENT_SEP) {
|
|
74
|
+
return i;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return sepIdx;
|
|
78
|
+
};
|
|
79
|
+
/** As above, or -1 when the JID has no server part: the case `jidDecode` reports as `undefined`. */
|
|
80
|
+
const userEnd = (jid) => {
|
|
81
|
+
const sepIdx = jid.indexOf('@');
|
|
82
|
+
return sepIdx < 0 ? -1 : userEndBefore(jid, sepIdx);
|
|
83
|
+
};
|
|
60
84
|
/** Compare the user component of two JIDs, matching upstream Baileys. */
|
|
61
|
-
export const areJidsSameUser = (jid1, jid2) =>
|
|
85
|
+
export const areJidsSameUser = (jid1, jid2) => {
|
|
86
|
+
// Same answer as comparing `jidDecode(...)?.user`, compared in place: this
|
|
87
|
+
// runs once per participant check on the message pipeline, and decoding both
|
|
88
|
+
// sides allocated four arrays and two objects only to throw them away.
|
|
89
|
+
const end = typeof jid1 === 'string' ? userEnd(jid1) : -1;
|
|
90
|
+
if (end !== (typeof jid2 === 'string' ? userEnd(jid2) : -1)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// Neither side decodes, so upstream compares `undefined === undefined`.
|
|
94
|
+
if (end < 0) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const left = jid1;
|
|
98
|
+
const right = jid2;
|
|
99
|
+
for (let i = 0; i < end; i++) {
|
|
100
|
+
if (left.charCodeAt(i) !== right.charCodeAt(i)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
62
106
|
export const isJidMetaAI = (jid) => jid?.endsWith('@bot');
|
|
63
107
|
export const isPnUser = (jid) => jid?.endsWith('@s.whatsapp.net');
|
|
64
108
|
export const isLidUser = (jid) => jid?.endsWith('@lid');
|
|
@@ -71,12 +115,25 @@ export const isHostedPnUser = (jid) => jid?.endsWith('@hosted');
|
|
|
71
115
|
const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/;
|
|
72
116
|
export const isJidBot = (jid) => jid && botRegexp.test(jid.split('@')[0]) && jid.endsWith('@c.us');
|
|
73
117
|
export const jidNormalizedUser = (jid) => {
|
|
74
|
-
|
|
75
|
-
|
|
118
|
+
if (typeof jid !== 'string') {
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
const sepIdx = jid.indexOf('@');
|
|
122
|
+
if (sepIdx < 0) {
|
|
76
123
|
return '';
|
|
77
124
|
}
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
// Everything between the user and the '@' is exactly what normalization
|
|
126
|
+
// drops, so the boundary is all this needs. `jidDecode` would split the same
|
|
127
|
+
// half twice and box the pieces in an object only for them to be re-joined.
|
|
128
|
+
const end = userEndBefore(jid, sepIdx);
|
|
129
|
+
// Fast path for the shape that dominates the pipeline: nothing to strip and a
|
|
130
|
+
// server `jidEncode` re-emits verbatim, so the JID already is its own normal
|
|
131
|
+
// form and no new string has to be built at all.
|
|
132
|
+
if (end === sepIdx && !jid.endsWith('@c.us')) {
|
|
133
|
+
return jid;
|
|
134
|
+
}
|
|
135
|
+
const server = jid.slice(sepIdx + 1);
|
|
136
|
+
return jidEncode(jid.slice(0, end), server === 'c.us' ? 's.whatsapp.net' : server);
|
|
80
137
|
};
|
|
81
138
|
export const transferDevice = (fromJid, toJid) => {
|
|
82
139
|
const deviceId = jidDecode(fromJid)?.device || 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxidezap/baileyrs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.9",
|
|
5
5
|
"description": "A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
},
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@hapi/boom": "^9.1.4",
|
|
85
|
-
"@oxidezap/whatsapp-rust-bridge": "0.
|
|
85
|
+
"@oxidezap/whatsapp-rust-bridge": "0.18.0",
|
|
86
86
|
"long": "^5.3.2",
|
|
87
87
|
"pino": "^10.3.1",
|
|
88
88
|
"protobufjs": "^7.6.5"
|