@oxidezap/baileyrs 0.2.10 → 0.2.12
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 +36 -0
- package/lib/Bridge/index.d.ts +1 -1
- package/lib/Bridge/index.js +1 -1
- package/lib/Bridge/primitives.d.ts +18 -6
- package/lib/Bridge/primitives.js +26 -13
- package/lib/Bridge/schema.js +103 -16
- package/lib/Bridge/types.d.ts +11 -0
- package/lib/Compatibility/proto-runtime.js +18 -9
- package/lib/Socket/index.js +7 -1
- package/lib/Socket/terminal-close-reporter.d.ts +29 -8
- package/lib/Socket/terminal-close-reporter.js +31 -8
- package/lib/Socket/unsupported-config.d.ts +1 -1
- package/lib/Socket/unsupported-config.js +1 -0
- package/lib/Types/Socket.d.ts +8 -0
- package/lib/Utils/use-bridge-store.d.ts +74 -3
- package/lib/Utils/use-bridge-store.js +541 -201
- package/lib/WAProto/compatibility-schema.d.ts +1 -1
- package/lib/WAProto/compatibility-schema.js +1 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@oxidezap/baileyrs)
|
|
4
4
|
[](https://www.npmjs.com/package/@oxidezap/baileyrs)
|
|
5
5
|
[](https://deepwiki.com/oxidezap/baileyrs)
|
|
6
|
+
[](https://pkg.pr.new/~/oxidezap/baileyrs)
|
|
6
7
|
|
|
7
8
|
A Rust-powered WhatsApp Web library for JavaScript, with a Baileys-compatible API.
|
|
8
9
|
|
|
@@ -87,6 +88,41 @@ That writes the alias to your `package.json` (with the latest version at install
|
|
|
87
88
|
Every `import { makeWASocket } from '@whiskeysockets/baileys'` in your codebase
|
|
88
89
|
now resolves to baileyrs.
|
|
89
90
|
|
|
91
|
+
### Preview builds
|
|
92
|
+
|
|
93
|
+
Every commit on `main` and every pull request publishes an installable build
|
|
94
|
+
through [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). Nothing is
|
|
95
|
+
published to npm — the tarball is served from a URL, so a fix can be tried
|
|
96
|
+
before it reaches a release:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
# the head of a pull request, following it as new commits land
|
|
100
|
+
npm install https://pkg.pr.new/@oxidezap/baileyrs@94
|
|
101
|
+
|
|
102
|
+
# one specific commit
|
|
103
|
+
npm install https://pkg.pr.new/@oxidezap/baileyrs@f59ae6f
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Projects using the Baileys alias point it at the same URL:
|
|
107
|
+
|
|
108
|
+
```jsonc
|
|
109
|
+
{
|
|
110
|
+
"dependencies": {
|
|
111
|
+
"@whiskeysockets/baileys": "https://pkg.pr.new/@oxidezap/baileyrs@94"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every open pull request comments the URL for its own head, and
|
|
117
|
+
[pkg.pr.new/~/oxidezap/baileyrs](https://pkg.pr.new/~/oxidezap/baileyrs) lists
|
|
118
|
+
what is available.
|
|
119
|
+
|
|
120
|
+
Preview builds carry the version `0.0.0-preview-<sha>`, which no range written
|
|
121
|
+
for a real release can match — an install is a deliberate pin, and it stays on
|
|
122
|
+
that commit until you change it. They are for trying a change, not for running
|
|
123
|
+
one: they are unreleased code, and the URL is not a substitute for a published
|
|
124
|
+
version in anything that has to keep resolving.
|
|
125
|
+
|
|
90
126
|
## Quick Start
|
|
91
127
|
|
|
92
128
|
```ts
|
package/lib/Bridge/index.d.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { adaptBridgeEvent, adaptBridgeMessageWire, KNOWN_BRIDGE_EVENT_TYPES } from './adapt.js';
|
|
10
10
|
export * from './types.js';
|
|
11
|
-
export { asBool, asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
11
|
+
export { asBool, asBoolOr, asJidString, asNumber, asString, asUnixSeconds, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/Bridge/index.js
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export { adaptBridgeEvent, adaptBridgeMessageWire, KNOWN_BRIDGE_EVENT_TYPES } from './adapt.js';
|
|
10
10
|
export * from './types.js';
|
|
11
|
-
export { asBool, asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
11
|
+
export { asBool, asBoolOr, asJidString, asNumber, asString, asUnixSeconds, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -50,16 +50,28 @@ export declare const asJidAddressString: (x: unknown) => string | undefined;
|
|
|
50
50
|
* strings — the bridge serializes `DateTime<Utc>` as a string unless the field
|
|
51
51
|
* names one of chrono's `ts_*` modules, so being lenient about which of the two
|
|
52
52
|
* arrives insulates us from drift.
|
|
53
|
+
*
|
|
54
|
+
* Absence and unparseable input stay absent (`undefined`): a missing,
|
|
55
|
+
* malformed, or non-finite timestamp must never become the epoch, which
|
|
56
|
+
* would place the event at 1970 and corrupt timeline ordering. Callers
|
|
57
|
+
* apply the per-field policy explicitly — required timestamps (message,
|
|
58
|
+
* receipt, call, group action) drop the event, optional ones (presence
|
|
59
|
+
* `last_seen`, server-ack time, pin/mute time) omit the field.
|
|
53
60
|
*/
|
|
54
|
-
export declare const
|
|
61
|
+
export declare const asUnixSeconds: (raw: unknown) => number | undefined;
|
|
55
62
|
/**
|
|
56
|
-
*
|
|
63
|
+
* Validated coercion to unix seconds for external consumers that need a
|
|
64
|
+
* plain number (no `undefined` branch).
|
|
57
65
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
66
|
+
* Accepts the same inputs as `asUnixSeconds` — a finite number or an RFC
|
|
67
|
+
* 3339 string — and rejects everything else by throwing `RangeError`
|
|
68
|
+
* instead of fabricating the epoch: a missing, malformed, or non-finite
|
|
69
|
+
* timestamp must never become 1970-01-01 and corrupt timeline ordering.
|
|
70
|
+
* Internal adapters do not use this; they take `asUnixSeconds` with an
|
|
71
|
+
* explicit per-field policy (required timestamps drop the event, optional
|
|
72
|
+
* ones omit the field).
|
|
61
73
|
*/
|
|
62
|
-
export declare const
|
|
74
|
+
export declare const toUnixSeconds: (raw: unknown) => number;
|
|
63
75
|
/**
|
|
64
76
|
* A 64-bit proto field, however the bridge chose to carry it.
|
|
65
77
|
*
|
package/lib/Bridge/primitives.js
CHANGED
|
@@ -82,20 +82,13 @@ const parseRfc3339Seconds = (raw) => {
|
|
|
82
82
|
* strings — the bridge serializes `DateTime<Utc>` as a string unless the field
|
|
83
83
|
* names one of chrono's `ts_*` modules, so being lenient about which of the two
|
|
84
84
|
* arrives insulates us from drift.
|
|
85
|
-
*/
|
|
86
|
-
export const toUnixSeconds = (raw) => {
|
|
87
|
-
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
88
|
-
return raw;
|
|
89
|
-
if (typeof raw === 'string')
|
|
90
|
-
return parseRfc3339Seconds(raw) ?? 0;
|
|
91
|
-
return 0;
|
|
92
|
-
};
|
|
93
|
-
/**
|
|
94
|
-
* Same as `toUnixSeconds`, but absence stays absent.
|
|
95
85
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
86
|
+
* Absence and unparseable input stay absent (`undefined`): a missing,
|
|
87
|
+
* malformed, or non-finite timestamp must never become the epoch, which
|
|
88
|
+
* would place the event at 1970 and corrupt timeline ordering. Callers
|
|
89
|
+
* apply the per-field policy explicitly — required timestamps (message,
|
|
90
|
+
* receipt, call, group action) drop the event, optional ones (presence
|
|
91
|
+
* `last_seen`, server-ack time, pin/mute time) omit the field.
|
|
99
92
|
*/
|
|
100
93
|
export const asUnixSeconds = (raw) => {
|
|
101
94
|
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
@@ -104,6 +97,26 @@ export const asUnixSeconds = (raw) => {
|
|
|
104
97
|
return parseRfc3339Seconds(raw);
|
|
105
98
|
return undefined;
|
|
106
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Validated coercion to unix seconds for external consumers that need a
|
|
102
|
+
* plain number (no `undefined` branch).
|
|
103
|
+
*
|
|
104
|
+
* Accepts the same inputs as `asUnixSeconds` — a finite number or an RFC
|
|
105
|
+
* 3339 string — and rejects everything else by throwing `RangeError`
|
|
106
|
+
* instead of fabricating the epoch: a missing, malformed, or non-finite
|
|
107
|
+
* timestamp must never become 1970-01-01 and corrupt timeline ordering.
|
|
108
|
+
* Internal adapters do not use this; they take `asUnixSeconds` with an
|
|
109
|
+
* explicit per-field policy (required timestamps drop the event, optional
|
|
110
|
+
* ones omit the field).
|
|
111
|
+
*/
|
|
112
|
+
export const toUnixSeconds = (raw) => {
|
|
113
|
+
const parsed = asUnixSeconds(raw);
|
|
114
|
+
if (parsed === undefined) {
|
|
115
|
+
const preview = typeof raw === 'string' ? `: ${raw.slice(0, 80)}` : '';
|
|
116
|
+
throw new RangeError(`toUnixSeconds: cannot coerce ${typeof raw}${preview} to unix seconds`);
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
};
|
|
107
120
|
/**
|
|
108
121
|
* A 64-bit proto field, however the bridge chose to carry it.
|
|
109
122
|
*
|
package/lib/Bridge/schema.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { processHistoryMessage } from '../Utils/process-history-message.js';
|
|
26
26
|
import { isJidGroup } from '../WABinary/jid-utils.js';
|
|
27
|
-
import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator
|
|
27
|
+
import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator } from './primitives.js';
|
|
28
28
|
/**
|
|
29
29
|
* Bridge sync-action events (`pin_update`, `mute_update`, etc.) carry the
|
|
30
30
|
* proto action under `data.action`, but the bridge `.d.ts` types it as
|
|
@@ -39,6 +39,17 @@ const extractAction = (data) => isObject(data?.action) ? data.action : undefined
|
|
|
39
39
|
const resolveIsGroup = (wireValue, chatJid) => asBoolOr(wireValue, false) || isJidGroup(chatJid) === true;
|
|
40
40
|
const resolveParticipantAlt = (senderAlt, isGroup) => isGroup ? senderAlt : undefined;
|
|
41
41
|
const resolveRemoteJidAlt = (senderAlt, recipientAlt, isGroup, isFromMe) => (isGroup ? undefined : isFromMe ? recipientAlt : senderAlt);
|
|
42
|
+
/**
|
|
43
|
+
* Rejection diagnostic for a required timestamp that is missing or
|
|
44
|
+
* unparseable. Metadata only — never the payload: these objects can carry
|
|
45
|
+
* message bodies and JIDs.
|
|
46
|
+
*/
|
|
47
|
+
const invalidTimestampDetail = (event, raw) => ({
|
|
48
|
+
event,
|
|
49
|
+
field: 'timestamp',
|
|
50
|
+
reason: raw === undefined || raw === null ? 'missing' : 'invalid',
|
|
51
|
+
receivedType: typeof raw
|
|
52
|
+
});
|
|
42
53
|
const parseEditAttribute = (value) => {
|
|
43
54
|
switch (value) {
|
|
44
55
|
case '1':
|
|
@@ -141,7 +152,7 @@ const ADAPTERS = {
|
|
|
141
152
|
error: asString(data?.error)
|
|
142
153
|
};
|
|
143
154
|
},
|
|
144
|
-
undecryptable_message: data => {
|
|
155
|
+
undecryptable_message: (data, logger) => {
|
|
145
156
|
// Bridge ships `{ info: MessageInfo, is_unavailable, unavailable_type,
|
|
146
157
|
// decrypt_fail_mode }` — same MessageInfo shape as the regular
|
|
147
158
|
// `message` event. Extract the chat / sender / id so the dispatcher
|
|
@@ -161,12 +172,17 @@ const ADAPTERS = {
|
|
|
161
172
|
const isFromMe = asBoolOr(src.is_from_me, false);
|
|
162
173
|
const senderAlt = asJidString(src.sender_alt);
|
|
163
174
|
const recipientAlt = asJidString(src.recipient_alt);
|
|
175
|
+
const timestamp = asUnixSeconds(info.timestamp);
|
|
176
|
+
if (timestamp === undefined) {
|
|
177
|
+
logger?.debug(invalidTimestampDetail('undecryptable_message', info.timestamp), 'undecryptable_message adapter: missing or invalid timestamp');
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
164
180
|
return {
|
|
165
181
|
type: 'undecryptableMessage',
|
|
166
182
|
chatJid: chat,
|
|
167
183
|
senderJid: isGroup ? asJidString(src.sender) : undefined,
|
|
168
184
|
id,
|
|
169
|
-
timestamp
|
|
185
|
+
timestamp,
|
|
170
186
|
isFromMe,
|
|
171
187
|
isGroup,
|
|
172
188
|
pushName: asString(info.push_name),
|
|
@@ -386,28 +402,38 @@ const ADAPTERS = {
|
|
|
386
402
|
call_log_sync: () => ({ type: 'noop', bridgeType: 'call_log_sync' }),
|
|
387
403
|
// ── Calls ──
|
|
388
404
|
incoming_call: (data, logger) => adaptIncomingCall(data, logger),
|
|
389
|
-
missed_call: data => {
|
|
405
|
+
missed_call: (data, logger) => {
|
|
390
406
|
const from = asJidString(data?.from);
|
|
391
407
|
const callId = asString(data?.call_id);
|
|
392
408
|
if (!from || !callId)
|
|
393
409
|
return null;
|
|
410
|
+
const timestamp = asUnixSeconds(data?.timestamp);
|
|
411
|
+
if (timestamp === undefined) {
|
|
412
|
+
logger?.debug(invalidTimestampDetail('missed_call', data?.timestamp), 'missed_call adapter: missing or invalid timestamp');
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
394
415
|
return {
|
|
395
416
|
type: 'incomingCall',
|
|
396
417
|
from,
|
|
397
|
-
timestamp
|
|
418
|
+
timestamp,
|
|
398
419
|
offline: data?.reason === 'offline',
|
|
399
420
|
action: { type: 'timeout', callId }
|
|
400
421
|
};
|
|
401
422
|
},
|
|
402
|
-
call_ended_elsewhere: data => {
|
|
423
|
+
call_ended_elsewhere: (data, logger) => {
|
|
403
424
|
const from = asJidString(data?.from);
|
|
404
425
|
const callId = asString(data?.call_id);
|
|
405
426
|
if (!from || !callId)
|
|
406
427
|
return null;
|
|
428
|
+
const timestamp = asUnixSeconds(data?.timestamp);
|
|
429
|
+
if (timestamp === undefined) {
|
|
430
|
+
logger?.debug(invalidTimestampDetail('call_ended_elsewhere', data?.timestamp), 'call_ended_elsewhere adapter: missing or invalid timestamp');
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
407
433
|
return {
|
|
408
434
|
type: 'incomingCall',
|
|
409
435
|
from,
|
|
410
|
-
timestamp
|
|
436
|
+
timestamp,
|
|
411
437
|
offline: false,
|
|
412
438
|
action: { type: data?.outcome === 'accepted' ? 'accept' : 'reject', callId }
|
|
413
439
|
};
|
|
@@ -458,6 +484,11 @@ const ADAPTERS = {
|
|
|
458
484
|
count: asNumber(data?.count) ?? 0
|
|
459
485
|
}),
|
|
460
486
|
offline_sync_preview: () => ({ type: 'noop', bridgeType: 'offline_sync_preview' }),
|
|
487
|
+
// Counterpart of `offline_sync_completed`, not a variant: the drain ended
|
|
488
|
+
// without its end marker, so the client is *not* caught up and the backlog
|
|
489
|
+
// is redelivered on the next connection. Emitting `receivedPendingNotifications`
|
|
490
|
+
// here would be a lie, so this stays a noop until the next preview/completion.
|
|
491
|
+
offline_sync_interrupted: () => ({ type: 'noop', bridgeType: 'offline_sync_interrupted' }),
|
|
461
492
|
dirty_state: data => {
|
|
462
493
|
const dirtyType = asString(data?.dirty_type);
|
|
463
494
|
if (!dirtyType)
|
|
@@ -675,6 +706,15 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
|
|
|
675
706
|
const isFromMe = asBoolOr(info.isFromMe, false);
|
|
676
707
|
const senderAlt = asString(info.senderAlt);
|
|
677
708
|
const recipientAlt = asString(info.recipientAlt);
|
|
709
|
+
// The packed envelope carries the timestamp as a numeric record, so the
|
|
710
|
+
// only invalid shapes here are a missing or non-finite value. Like the
|
|
711
|
+
// object route below, those drop the message rather than dating it at
|
|
712
|
+
// the epoch.
|
|
713
|
+
const timestamp = asNumber(info.timestamp);
|
|
714
|
+
if (timestamp === undefined) {
|
|
715
|
+
logger?.debug(invalidTimestampDetail('message_wire', info.timestamp), 'message wire adapter: missing or invalid timestamp');
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
678
718
|
return {
|
|
679
719
|
type: 'message',
|
|
680
720
|
chatJid: chat,
|
|
@@ -682,7 +722,7 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
|
|
|
682
722
|
isGroup,
|
|
683
723
|
isFromMe,
|
|
684
724
|
id,
|
|
685
|
-
timestamp
|
|
725
|
+
timestamp,
|
|
686
726
|
pushName: asString(info.pushName),
|
|
687
727
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
688
728
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
@@ -707,6 +747,11 @@ const adaptMessageParts = (info, messageProto, logger) => {
|
|
|
707
747
|
const senderJid = isGroup ? asJidString(senderRaw) : undefined;
|
|
708
748
|
const senderAlt = asJidString(src.sender_alt);
|
|
709
749
|
const recipientAlt = asJidString(src.recipient_alt);
|
|
750
|
+
const timestamp = asUnixSeconds(info.timestamp);
|
|
751
|
+
if (timestamp === undefined) {
|
|
752
|
+
logger?.debug(invalidTimestampDetail('message', info.timestamp), 'message adapter: missing or invalid timestamp');
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
710
755
|
return {
|
|
711
756
|
type: 'message',
|
|
712
757
|
chatJid: chat,
|
|
@@ -714,7 +759,7 @@ const adaptMessageParts = (info, messageProto, logger) => {
|
|
|
714
759
|
isGroup,
|
|
715
760
|
isFromMe,
|
|
716
761
|
id,
|
|
717
|
-
timestamp
|
|
762
|
+
timestamp,
|
|
718
763
|
pushName: asString(info.push_name),
|
|
719
764
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
720
765
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
@@ -738,6 +783,16 @@ const adaptReceipt = (data, logger) => {
|
|
|
738
783
|
return null;
|
|
739
784
|
}
|
|
740
785
|
const isGroup = resolveIsGroup(src.is_group, chat);
|
|
786
|
+
// A receipt without a trustworthy timestamp cannot be placed on any
|
|
787
|
+
// timeline slot (`readTimestamp` / `playedTimestamp` /
|
|
788
|
+
// `receiptTimestamp`), so the event is dropped rather than dated at the
|
|
789
|
+
// epoch. Optional timestamps elsewhere stay absent via `asUnixSeconds`.
|
|
790
|
+
const timestamp = asUnixSeconds(data.timestamp);
|
|
791
|
+
if (timestamp === undefined) {
|
|
792
|
+
logger?.debug(invalidTimestampDetail('receipt', data.timestamp), 'receipt adapter: missing or invalid timestamp');
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
const { receiptType, raw: receiptTypeRaw } = parseReceiptType(data.type);
|
|
741
796
|
return {
|
|
742
797
|
type: 'receipt',
|
|
743
798
|
chatJid: chat,
|
|
@@ -745,8 +800,9 @@ const adaptReceipt = (data, logger) => {
|
|
|
745
800
|
isGroup,
|
|
746
801
|
isFromMe: asBoolOr(src.is_from_me, false),
|
|
747
802
|
messageIds: ids,
|
|
748
|
-
timestamp
|
|
749
|
-
receiptType
|
|
803
|
+
timestamp,
|
|
804
|
+
receiptType,
|
|
805
|
+
...(receiptTypeRaw !== undefined ? { receiptTypeRaw } : {})
|
|
750
806
|
};
|
|
751
807
|
};
|
|
752
808
|
const adaptContactUpdate = (data) => {
|
|
@@ -767,7 +823,8 @@ const adaptContactUpdate = (data) => {
|
|
|
767
823
|
};
|
|
768
824
|
};
|
|
769
825
|
/**
|
|
770
|
-
* Bridge wire `ReceiptType` → canonical kebab-cased variant
|
|
826
|
+
* Bridge wire `ReceiptType` → canonical kebab-cased variant, preserving the
|
|
827
|
+
* original value when it matches nothing known.
|
|
771
828
|
*
|
|
772
829
|
* The bridge's generated `.d.ts` advertises `ReceiptType` as
|
|
773
830
|
* `{type: "delivered"} | …`, but `#[serde(from = "String")]` on the rust
|
|
@@ -775,6 +832,12 @@ const adaptContactUpdate = (data) => {
|
|
|
775
832
|
* bare PascalCase variant name (`"Delivered"`, `"Read"`, `"PeerMsg"`).
|
|
776
833
|
* Keep both spellings here so a future bridge bump that re-introduces
|
|
777
834
|
* the snake_case wire form keeps working.
|
|
835
|
+
*
|
|
836
|
+
* Unrecognized wire values arrive wrapped as `{ Other: value }` (the packed
|
|
837
|
+
* `ReceiptWireData.type` contract) or, defensively, as a future bare
|
|
838
|
+
* string. Those map to `'other'` with the original value in `raw`. The
|
|
839
|
+
* wrapper is trusted over spelling: `{ Other: 'Read' }` is `other`, not
|
|
840
|
+
* `read` (see `parseReceiptType`).
|
|
778
841
|
*/
|
|
779
842
|
const RECEIPT_TYPE_MAP = {
|
|
780
843
|
Delivered: 'delivered',
|
|
@@ -805,10 +868,22 @@ const RECEIPT_TYPE_MAP = {
|
|
|
805
868
|
server_error: 'server-error'
|
|
806
869
|
};
|
|
807
870
|
const parseReceiptType = (raw) => {
|
|
871
|
+
// The `Other` wrapper is authoritative: the packed codec round-trips
|
|
872
|
+
// `{ Other: value }` verbatim and never synthesizes it for a known
|
|
873
|
+
// variant, so even a payload whose spelling collides with a known
|
|
874
|
+
// variant name (e.g. `{ Other: 'Read' }`) stays category `other` with
|
|
875
|
+
// its exact payload preserved. Only bare strings and `{ type }` go
|
|
876
|
+
// through the known-variant lookup.
|
|
877
|
+
if (isObject(raw) && typeof raw.Other === 'string')
|
|
878
|
+
return { receiptType: 'other', raw: raw.Other };
|
|
808
879
|
const norm = typeof raw === 'string' ? raw : isObject(raw) && typeof raw.type === 'string' ? raw.type : undefined;
|
|
809
880
|
if (norm == null)
|
|
810
|
-
return undefined;
|
|
811
|
-
|
|
881
|
+
return { receiptType: undefined };
|
|
882
|
+
// Own-property lookup: the table is an ordinary object, so indexing an
|
|
883
|
+
// unknown bare name like 'constructor' or '__proto__' would otherwise
|
|
884
|
+
// return an inherited value instead of falling through to 'other'.
|
|
885
|
+
const mapped = Object.hasOwn(RECEIPT_TYPE_MAP, norm) ? RECEIPT_TYPE_MAP[norm] : undefined;
|
|
886
|
+
return mapped ? { receiptType: mapped } : { receiptType: 'other', raw: norm };
|
|
812
887
|
};
|
|
813
888
|
const adaptStarUpdate = (data) => {
|
|
814
889
|
if (!isObject(data))
|
|
@@ -849,6 +924,13 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
849
924
|
logger?.debug({ data }, 'incoming_call adapter: missing action.type/call_id');
|
|
850
925
|
return null;
|
|
851
926
|
}
|
|
927
|
+
// The call offer is timeline-ordered by its timestamp downstream; an
|
|
928
|
+
// unparseable one drops the event rather than dating it at the epoch.
|
|
929
|
+
const timestamp = asUnixSeconds(data.timestamp);
|
|
930
|
+
if (timestamp === undefined) {
|
|
931
|
+
logger?.debug(invalidTimestampDetail('incoming_call', data.timestamp), 'incoming_call adapter: missing or invalid timestamp');
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
852
934
|
const canonicalAction = {
|
|
853
935
|
type: actionType,
|
|
854
936
|
callId,
|
|
@@ -871,7 +953,7 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
871
953
|
return {
|
|
872
954
|
type: 'incomingCall',
|
|
873
955
|
from: asJidAddressString(data.from) ?? from,
|
|
874
|
-
timestamp
|
|
956
|
+
timestamp,
|
|
875
957
|
offline: asBoolOr(data.offline, false),
|
|
876
958
|
stanzaId: asString(data.stanza_id),
|
|
877
959
|
notify: asString(data.notify),
|
|
@@ -1103,6 +1185,11 @@ const adaptGroupUpdate = (data, logger) => {
|
|
|
1103
1185
|
logger?.warn({ data }, 'group_update adapter: action shape rejected');
|
|
1104
1186
|
return null;
|
|
1105
1187
|
}
|
|
1188
|
+
const timestamp = asUnixSeconds(data.timestamp);
|
|
1189
|
+
if (timestamp === undefined) {
|
|
1190
|
+
logger?.debug(invalidTimestampDetail('group_update', data.timestamp), 'group_update adapter: missing or invalid timestamp');
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1106
1193
|
return {
|
|
1107
1194
|
type: 'groupUpdate',
|
|
1108
1195
|
groupJid,
|
|
@@ -1112,7 +1199,7 @@ const adaptGroupUpdate = (data, logger) => {
|
|
|
1112
1199
|
authorPn: asJidString(data.participant_pn),
|
|
1113
1200
|
authorUsername: asString(data.participant_username),
|
|
1114
1201
|
authorCountryCode: asString(data.participant_country_code),
|
|
1115
|
-
timestamp
|
|
1202
|
+
timestamp,
|
|
1116
1203
|
isLidAddressingMode: asBoolOr(data.is_lid_addressing_mode, false),
|
|
1117
1204
|
action
|
|
1118
1205
|
};
|
package/lib/Bridge/types.d.ts
CHANGED
|
@@ -172,6 +172,17 @@ export interface CanonicalReceipt {
|
|
|
172
172
|
* `readTimestamp` / `playedTimestamp` slot in the emitted update.
|
|
173
173
|
*/
|
|
174
174
|
receiptType?: 'delivered' | 'sent' | 'sender' | 'retry' | 'enc-rekey-retry' | 'read' | 'read-self' | 'played' | 'played-self' | 'inactive' | 'peer-msg' | 'history-sync' | 'server-error' | 'other';
|
|
175
|
+
/**
|
|
176
|
+
* Original wire value when `receiptType` is `'other'` — the bridge sends
|
|
177
|
+
* unrecognized variants as `{ Other: value }` (or a future bare string),
|
|
178
|
+
* and the category alone would lose which one it was. Always set for an
|
|
179
|
+
* `Other`-wrapped payload, even when its spelling collides with a known
|
|
180
|
+
* variant name: the wrapper means the producer did not recognize it.
|
|
181
|
+
* Absent whenever the value matched a known variant through a bare
|
|
182
|
+
* string or `{ type }` shape, and whenever `receiptType` itself is
|
|
183
|
+
* absent.
|
|
184
|
+
*/
|
|
185
|
+
receiptTypeRaw?: string;
|
|
175
186
|
}
|
|
176
187
|
export interface CanonicalContactUpdate {
|
|
177
188
|
type: 'contactUpdate';
|
|
@@ -6,8 +6,7 @@ const INSTANCE_SCHEMA = Symbol('proto compatibility schema');
|
|
|
6
6
|
const EMPTY_ARRAY = Object.freeze([]);
|
|
7
7
|
const EMPTY_OBJECT = Object.freeze({});
|
|
8
8
|
const JSON_OPTIONS = Object.freeze({ longs: String, enums: String, bytes: String, json: true });
|
|
9
|
-
const
|
|
10
|
-
const WORD_BASE = 1n << BigInt(WORD_BITS);
|
|
9
|
+
const WORD_BASE = 1n << 32n;
|
|
11
10
|
// protobufjs resolves the CommonJS Long constructor internally. Loading that
|
|
12
11
|
// same export keeps `instanceof` and prototype identity aligned without
|
|
13
12
|
// loading protobufjs itself or adding a second wire runtime.
|
|
@@ -79,12 +78,25 @@ const repairScalar = (kind, item) => {
|
|
|
79
78
|
return replaced === item ? item : replaced;
|
|
80
79
|
};
|
|
81
80
|
const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, unsigned);
|
|
81
|
+
/**
|
|
82
|
+
* Split a decoded 64-bit value into the low/high words `longFromWords` takes.
|
|
83
|
+
* Shifts and masks are exact on BigInt at any magnitude, and `BigInt(string)`
|
|
84
|
+
* parses exactly, so the `string` leg the reader's types allow costs precision
|
|
85
|
+
* nothing — only the one intermediate allocation.
|
|
86
|
+
*/
|
|
87
|
+
const wordsFromInt64 = (value) => {
|
|
88
|
+
const v = typeof value === 'bigint' ? value : BigInt(value);
|
|
89
|
+
return [Number(v & 0xffffffffn), Number((v >> 32n) & 0xffffffffn)];
|
|
90
|
+
};
|
|
82
91
|
/**
|
|
83
92
|
* The neutral codec returns a JS number while a 64-bit value is exact as a
|
|
84
93
|
* double and a plain `{ low, high, unsigned }` past that. The compatibility
|
|
85
94
|
* facade supplies this reader so the same generated decoder materializes every
|
|
86
95
|
* 64-bit word as a long.js Long instead — uniformly, whatever the magnitude —
|
|
87
|
-
*
|
|
96
|
+
* through one decode. `@bufbuild/protobuf` 2.14 privatized the word-level
|
|
97
|
+
* varint reader the overrides used to share, so each 64-bit varint now passes
|
|
98
|
+
* through one BigInt intermediate; uniformity is what remains, and precision
|
|
99
|
+
* is unchanged.
|
|
88
100
|
*
|
|
89
101
|
* Uniformity is the point: upstream's types declare `Long` for these fields,
|
|
90
102
|
* so a consumer calling `.toNumber()` must not have that work only for values
|
|
@@ -93,18 +105,15 @@ const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, u
|
|
|
93
105
|
*/
|
|
94
106
|
class LongBinaryReader extends BinaryReader {
|
|
95
107
|
uint64Value() {
|
|
96
|
-
const [low, high] = this.
|
|
108
|
+
const [low, high] = wordsFromInt64(this.uint64());
|
|
97
109
|
return longFromWords(low, high, true);
|
|
98
110
|
}
|
|
99
111
|
int64Value() {
|
|
100
|
-
const [low, high] = this.
|
|
112
|
+
const [low, high] = wordsFromInt64(this.int64());
|
|
101
113
|
return longFromWords(low, high, false);
|
|
102
114
|
}
|
|
103
115
|
sint64Value() {
|
|
104
|
-
|
|
105
|
-
const sign = -(low & 1);
|
|
106
|
-
low = ((low >>> 1) | ((high & 1) << (WORD_BITS - 1))) ^ sign;
|
|
107
|
-
high = (high >>> 1) ^ sign;
|
|
116
|
+
const [low, high] = wordsFromInt64(this.sint64());
|
|
108
117
|
return longFromWords(low, high, false);
|
|
109
118
|
}
|
|
110
119
|
fixed64Value() {
|
package/lib/Socket/index.js
CHANGED
|
@@ -454,7 +454,13 @@ const makeWASocket = (config) => {
|
|
|
454
454
|
}
|
|
455
455
|
if (useNativeMemory)
|
|
456
456
|
logger.debug('auth: using socket-local native memory backend');
|
|
457
|
-
const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null
|
|
457
|
+
const created = await createWhatsAppClient(makeTransport(fullConfig), makeHttpClient(fullConfig), eventHandlers, bridgeStore, fullConfig.cache ?? null, fullConfig.version, fullConfig.wantedPreKeyCount ?? null,
|
|
458
|
+
// Passed through exactly as configured, never normalized by truthiness:
|
|
459
|
+
// the bridge only honours a literal `true` here and rejects any
|
|
460
|
+
// other truthy value at construction, so a `!!`/ternary-style
|
|
461
|
+
// coercion could promote a malformed opt-out into an opt-in.
|
|
462
|
+
// Absent stays strict.
|
|
463
|
+
fullConfig.dangerSkipCertChainVerify);
|
|
458
464
|
// `end()` can land while the client is still being built — a `sock.end()`
|
|
459
465
|
// or `await using` right after `makeWASocket()` does exactly that. When
|
|
460
466
|
// it has, `adopt` frees this client and tells us to stop: nothing else
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reports the terminal `connection.update { close }` — exactly once,
|
|
3
|
-
*
|
|
2
|
+
* Reports the terminal `connection.update { close }` — exactly once, and
|
|
3
|
+
* never silently not at all.
|
|
4
|
+
*
|
|
5
|
+
* The accepted claim publishes after its teardown settles, so a consumer that
|
|
6
|
+
* answers the close with a replacement socket does not overlap the old one's
|
|
7
|
+
* release. The watchdog is the deliberate exception: past the timeout the
|
|
8
|
+
* close goes out with teardown still running (and logged), because losing the
|
|
9
|
+
* event entirely is the worse failure. Nothing here promises that every close
|
|
10
|
+
* lands after every resource is released — only that at most one close lands.
|
|
4
11
|
*
|
|
5
12
|
* That sentence is the whole contract this branch sells, and getting it wrong
|
|
6
13
|
* has two opposite failure modes, both bad:
|
|
@@ -8,9 +15,8 @@
|
|
|
8
15
|
* - **Not reported.** The consumer's handler never runs, so it never builds a
|
|
9
16
|
* replacement socket. A bot offline with nothing in its logs — the original
|
|
10
17
|
* bug this branch exists to fix.
|
|
11
|
-
* - **Reported
|
|
12
|
-
* one
|
|
13
|
-
* notifications for one socket and a handler that cleans up on close loops.
|
|
18
|
+
* - **Reported twice.** Every listener sees two terminal notifications for
|
|
19
|
+
* one socket and a handler that cleans up on close loops.
|
|
14
20
|
*
|
|
15
21
|
* Keeping both away used to be inline logic split across the dispatcher hook
|
|
16
22
|
* and `logout()`, sharing a counter and a promise. Nine separate bugs came out
|
|
@@ -41,13 +47,26 @@ export interface TerminalCloseReporter {
|
|
|
41
47
|
*
|
|
42
48
|
* Never rejects and never leaves `publish` uncalled: teardown failures are
|
|
43
49
|
* logged, a throwing listener is contained, and a teardown that hangs is
|
|
44
|
-
*
|
|
50
|
+
* reported past by the watchdog.
|
|
51
|
+
*
|
|
52
|
+
* Idempotent per socket: the first claim wins and every later call is
|
|
53
|
+
* ignored entirely — neither its teardown nor its publish is invoked. A
|
|
54
|
+
* socket never gets a second generation — a terminal close means "build a
|
|
55
|
+
* new socket" — so a second terminal event (a logout racing a dispatcher
|
|
56
|
+
* close, a late duplicate dispatch) must not publish again.
|
|
57
|
+
*
|
|
58
|
+
* The watchdog bounds how long `published()` waits, not the teardown
|
|
59
|
+
* itself: it neither cancels nor settles the underlying teardown, and a
|
|
60
|
+
* teardown that finishes late publishes nothing further.
|
|
45
61
|
*/
|
|
46
62
|
reportAfter: (teardown: () => Promise<void>, publish: () => void) => void;
|
|
47
63
|
/**
|
|
48
64
|
* Publish immediately, for a close nothing else will announce — a `logout()`
|
|
49
65
|
* with no live client, or one whose `logout()` threw before the bridge
|
|
50
66
|
* dispatched anything.
|
|
67
|
+
*
|
|
68
|
+
* Part of the same single claim as `reportAfter`: a no-op when a close
|
|
69
|
+
* was already claimed.
|
|
51
70
|
*/
|
|
52
71
|
reportNow: (publish: () => void) => void;
|
|
53
72
|
/**
|
|
@@ -63,11 +82,13 @@ export interface TerminalCloseReporter {
|
|
|
63
82
|
*/
|
|
64
83
|
hasReported: () => boolean;
|
|
65
84
|
/**
|
|
66
|
-
* Settles once the
|
|
85
|
+
* Settles once the single report has been published — the moment the
|
|
67
86
|
* consumer sees it, not the moment teardown finishes, so a waiter is not
|
|
68
87
|
* left hanging when the watchdog is what released the event.
|
|
69
88
|
*
|
|
70
|
-
* Resolves immediately when nothing has been reported.
|
|
89
|
+
* Resolves immediately when nothing has been reported. Every waiter
|
|
90
|
+
* observes the same promise, so concurrent `logout()` and dispatcher
|
|
91
|
+
* paths cannot split across generations.
|
|
71
92
|
*/
|
|
72
93
|
published: () => Promise<void>;
|
|
73
94
|
}
|