@oxidezap/baileyrs 0.2.11 → 0.2.13
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/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 +98 -16
- package/lib/Bridge/types.d.ts +11 -0
- package/lib/Compatibility/encode-proto.d.ts +7 -3
- package/lib/Compatibility/encode-proto.js +10 -5
- package/lib/Compatibility/proto-runtime.js +54 -7
- package/lib/Socket/events.js +1 -36
- package/lib/Socket/groups.js +35 -2
- package/lib/Socket/index.js +102 -18
- package/lib/Socket/terminal-close-reporter.d.ts +29 -8
- package/lib/Socket/terminal-close-reporter.js +31 -8
- package/lib/Socket/terminal-close.d.ts +2 -36
- package/lib/Socket/terminal-close.js +29 -5
- 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/package.json +3 -2
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
|
};
|
|
@@ -680,6 +706,15 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
|
|
|
680
706
|
const isFromMe = asBoolOr(info.isFromMe, false);
|
|
681
707
|
const senderAlt = asString(info.senderAlt);
|
|
682
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
|
+
}
|
|
683
718
|
return {
|
|
684
719
|
type: 'message',
|
|
685
720
|
chatJid: chat,
|
|
@@ -687,7 +722,7 @@ export const adaptBridgeMessageWire = (messageProto, info, logger) => {
|
|
|
687
722
|
isGroup,
|
|
688
723
|
isFromMe,
|
|
689
724
|
id,
|
|
690
|
-
timestamp
|
|
725
|
+
timestamp,
|
|
691
726
|
pushName: asString(info.pushName),
|
|
692
727
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
693
728
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
@@ -712,6 +747,11 @@ const adaptMessageParts = (info, messageProto, logger) => {
|
|
|
712
747
|
const senderJid = isGroup ? asJidString(senderRaw) : undefined;
|
|
713
748
|
const senderAlt = asJidString(src.sender_alt);
|
|
714
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
|
+
}
|
|
715
755
|
return {
|
|
716
756
|
type: 'message',
|
|
717
757
|
chatJid: chat,
|
|
@@ -719,7 +759,7 @@ const adaptMessageParts = (info, messageProto, logger) => {
|
|
|
719
759
|
isGroup,
|
|
720
760
|
isFromMe,
|
|
721
761
|
id,
|
|
722
|
-
timestamp
|
|
762
|
+
timestamp,
|
|
723
763
|
pushName: asString(info.push_name),
|
|
724
764
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
725
765
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
@@ -743,6 +783,16 @@ const adaptReceipt = (data, logger) => {
|
|
|
743
783
|
return null;
|
|
744
784
|
}
|
|
745
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);
|
|
746
796
|
return {
|
|
747
797
|
type: 'receipt',
|
|
748
798
|
chatJid: chat,
|
|
@@ -750,8 +800,9 @@ const adaptReceipt = (data, logger) => {
|
|
|
750
800
|
isGroup,
|
|
751
801
|
isFromMe: asBoolOr(src.is_from_me, false),
|
|
752
802
|
messageIds: ids,
|
|
753
|
-
timestamp
|
|
754
|
-
receiptType
|
|
803
|
+
timestamp,
|
|
804
|
+
receiptType,
|
|
805
|
+
...(receiptTypeRaw !== undefined ? { receiptTypeRaw } : {})
|
|
755
806
|
};
|
|
756
807
|
};
|
|
757
808
|
const adaptContactUpdate = (data) => {
|
|
@@ -772,7 +823,8 @@ const adaptContactUpdate = (data) => {
|
|
|
772
823
|
};
|
|
773
824
|
};
|
|
774
825
|
/**
|
|
775
|
-
* 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.
|
|
776
828
|
*
|
|
777
829
|
* The bridge's generated `.d.ts` advertises `ReceiptType` as
|
|
778
830
|
* `{type: "delivered"} | …`, but `#[serde(from = "String")]` on the rust
|
|
@@ -780,6 +832,12 @@ const adaptContactUpdate = (data) => {
|
|
|
780
832
|
* bare PascalCase variant name (`"Delivered"`, `"Read"`, `"PeerMsg"`).
|
|
781
833
|
* Keep both spellings here so a future bridge bump that re-introduces
|
|
782
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`).
|
|
783
841
|
*/
|
|
784
842
|
const RECEIPT_TYPE_MAP = {
|
|
785
843
|
Delivered: 'delivered',
|
|
@@ -810,10 +868,22 @@ const RECEIPT_TYPE_MAP = {
|
|
|
810
868
|
server_error: 'server-error'
|
|
811
869
|
};
|
|
812
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 };
|
|
813
879
|
const norm = typeof raw === 'string' ? raw : isObject(raw) && typeof raw.type === 'string' ? raw.type : undefined;
|
|
814
880
|
if (norm == null)
|
|
815
|
-
return undefined;
|
|
816
|
-
|
|
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 };
|
|
817
887
|
};
|
|
818
888
|
const adaptStarUpdate = (data) => {
|
|
819
889
|
if (!isObject(data))
|
|
@@ -854,6 +924,13 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
854
924
|
logger?.debug({ data }, 'incoming_call adapter: missing action.type/call_id');
|
|
855
925
|
return null;
|
|
856
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
|
+
}
|
|
857
934
|
const canonicalAction = {
|
|
858
935
|
type: actionType,
|
|
859
936
|
callId,
|
|
@@ -876,7 +953,7 @@ const adaptIncomingCall = (data, logger) => {
|
|
|
876
953
|
return {
|
|
877
954
|
type: 'incomingCall',
|
|
878
955
|
from: asJidAddressString(data.from) ?? from,
|
|
879
|
-
timestamp
|
|
956
|
+
timestamp,
|
|
880
957
|
offline: asBoolOr(data.offline, false),
|
|
881
958
|
stanzaId: asString(data.stanza_id),
|
|
882
959
|
notify: asString(data.notify),
|
|
@@ -1108,6 +1185,11 @@ const adaptGroupUpdate = (data, logger) => {
|
|
|
1108
1185
|
logger?.warn({ data }, 'group_update adapter: action shape rejected');
|
|
1109
1186
|
return null;
|
|
1110
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
|
+
}
|
|
1111
1193
|
return {
|
|
1112
1194
|
type: 'groupUpdate',
|
|
1113
1195
|
groupJid,
|
|
@@ -1117,7 +1199,7 @@ const adaptGroupUpdate = (data, logger) => {
|
|
|
1117
1199
|
authorPn: asJidString(data.participant_pn),
|
|
1118
1200
|
authorUsername: asString(data.participant_username),
|
|
1119
1201
|
authorCountryCode: asString(data.participant_country_code),
|
|
1120
|
-
timestamp
|
|
1202
|
+
timestamp,
|
|
1121
1203
|
isLidAddressingMode: asBoolOr(data.is_lid_addressing_mode, false),
|
|
1122
1204
|
action
|
|
1123
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';
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `encodeProto`, with the
|
|
2
|
+
* `encodeProto`, with the three inputs the bridge codec refuses put back.
|
|
3
3
|
*
|
|
4
4
|
* From 0.8.0 the codec refuses an empty string where the schema declares a
|
|
5
5
|
* 64-bit integer, and an unpaired surrogate in a text field. Both were written
|
|
6
6
|
* before — as `0` and as U+FFFD — and upstream Baileys still encodes both, so a
|
|
7
7
|
* message that used to reach the server would now throw in the caller's face.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The codec also refuses enum names where the schema declares an enum
|
|
9
|
+
* (issue #109: `"NONE"` where an int32 goes on the wire), which upstream's
|
|
10
|
+
* `fromObject` resolves and its direct `encode` coerces — that repair lives in
|
|
11
|
+
* `repairProtoMessage`, next to the other two. This is where all three are
|
|
12
|
+
* absorbed, so the strict contract stays true of the bridge and the tolerant
|
|
13
|
+
* one stays true of this library.
|
|
10
14
|
*
|
|
11
15
|
* Repair on failure rather than check on write: the ordinary encode is exactly
|
|
12
16
|
* the call it was before, with no scan of any field, and the repair runs only
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { encodeProto } from '@oxidezap/whatsapp-rust-bridge';
|
|
2
2
|
import { repairProtoMessage } from './proto-runtime.js';
|
|
3
3
|
/**
|
|
4
|
-
* `encodeProto`, with the
|
|
4
|
+
* `encodeProto`, with the three inputs the bridge codec refuses put back.
|
|
5
5
|
*
|
|
6
6
|
* From 0.8.0 the codec refuses an empty string where the schema declares a
|
|
7
7
|
* 64-bit integer, and an unpaired surrogate in a text field. Both were written
|
|
8
8
|
* before — as `0` and as U+FFFD — and upstream Baileys still encodes both, so a
|
|
9
9
|
* message that used to reach the server would now throw in the caller's face.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The codec also refuses enum names where the schema declares an enum
|
|
11
|
+
* (issue #109: `"NONE"` where an int32 goes on the wire), which upstream's
|
|
12
|
+
* `fromObject` resolves and its direct `encode` coerces — that repair lives in
|
|
13
|
+
* `repairProtoMessage`, next to the other two. This is where all three are
|
|
14
|
+
* absorbed, so the strict contract stays true of the bridge and the tolerant
|
|
15
|
+
* one stays true of this library.
|
|
12
16
|
*
|
|
13
17
|
* Repair on failure rather than check on write: the ordinary encode is exactly
|
|
14
18
|
* the call it was before, with no scan of any field, and the repair runs only
|
|
@@ -22,8 +26,9 @@ export const encodeProtoCompat = (path, message) => {
|
|
|
22
26
|
catch (error) {
|
|
23
27
|
const repaired = repairProtoMessage(path, message);
|
|
24
28
|
// Reference equality: nothing was coerced, so the failure is something this
|
|
25
|
-
// does not explain — an unmodelled type, a number no int64 can hold
|
|
26
|
-
// it has to keep propagating rather than be
|
|
29
|
+
// does not explain — an unmodelled type, a number no int64 can hold, an
|
|
30
|
+
// unknown enum name — and it has to keep propagating rather than be
|
|
31
|
+
// retried into a second throw.
|
|
27
32
|
if (repaired === message)
|
|
28
33
|
throw error;
|
|
29
34
|
return encodeProto(path, repaired);
|
|
@@ -53,7 +53,7 @@ const UNPAIRED_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<|(?<![\uD800-\uDBF
|
|
|
62
62
|
* - An unpaired surrogate in a text field, replaced with U+FFFD. That is the
|
|
63
63
|
* substitution `TextEncoder` used to make, so the bytes are unchanged from what
|
|
64
64
|
* this library sent before.
|
|
65
|
+
* - An enum name where the schema declares an enum (issue #109: `"NONE"` where
|
|
66
|
+
* an int32 goes on the wire). Upstream's `fromObject` resolves names to numbers
|
|
67
|
+
* and its direct `encode` coerces any string with `| 0`, so a caller passing a
|
|
68
|
+
* name never sees a throw. The bridge codec accepts only numbers (numeric
|
|
69
|
+
* strings aside) and throws `invalid int32: "NONE"`. An unknown name is left to
|
|
70
|
+
* throw rather than silenced to `0`: upstream's direct encode would write `0`
|
|
71
|
+
* for it, but that puts a value on the wire nobody sent.
|
|
65
72
|
*
|
|
66
73
|
* Returns `item` itself when it has nothing to do, so the caller can tell a
|
|
67
74
|
* repair from a failure it does not understand.
|
|
@@ -77,6 +84,36 @@ const repairScalar = (kind, item) => {
|
|
|
77
84
|
const replaced = item.replace(UNPAIRED_SURROGATE, '\uFFFD');
|
|
78
85
|
return replaced === item ? item : replaced;
|
|
79
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* Enum name to wire number, built per enum on the first repair that needs it.
|
|
89
|
+
*
|
|
90
|
+
* Only the repair path reads this, and only for a field that actually holds a
|
|
91
|
+
* string — a message the codec accepts never reaches here, and a numeric enum
|
|
92
|
+
* never touches a map. Each table is built once from the generated entries and
|
|
93
|
+
* then reused; an importer that never sends a refused value allocates nothing.
|
|
94
|
+
* A `Map` (not a plain object) so names like `__proto__` are keys, not hazards.
|
|
95
|
+
*/
|
|
96
|
+
let enumTablesById;
|
|
97
|
+
const enumValueFor = (enumId, name) => {
|
|
98
|
+
if (enumId < 0 || enumId >= PROTO_ENUM_SCHEMAS.length)
|
|
99
|
+
return undefined;
|
|
100
|
+
enumTablesById ?? (enumTablesById = []);
|
|
101
|
+
let byName = enumTablesById[enumId];
|
|
102
|
+
if (byName === undefined) {
|
|
103
|
+
const entries = PROTO_ENUM_SCHEMAS[enumId]?.[1];
|
|
104
|
+
if (!entries)
|
|
105
|
+
return undefined;
|
|
106
|
+
byName = new Map();
|
|
107
|
+
for (let index = 0; index < entries.length; index += 2) {
|
|
108
|
+
const entryName = entries[index];
|
|
109
|
+
const entryValue = entries[index + 1];
|
|
110
|
+
if (typeof entryName === 'string' && typeof entryValue === 'number')
|
|
111
|
+
byName.set(entryName, entryValue);
|
|
112
|
+
}
|
|
113
|
+
enumTablesById[enumId] = byName;
|
|
114
|
+
}
|
|
115
|
+
return byName.get(name);
|
|
116
|
+
};
|
|
80
117
|
const longFromWords = (low, high, unsigned) => LongRuntime.fromBits(low, high, unsigned);
|
|
81
118
|
/**
|
|
82
119
|
* Split a decoded 64-bit value into the low/high words `longFromWords` takes.
|
|
@@ -254,12 +291,13 @@ const schemaIdFor = (path) => {
|
|
|
254
291
|
return schemaIdsByPath.get(path);
|
|
255
292
|
};
|
|
256
293
|
/**
|
|
257
|
-
* Coerces the
|
|
258
|
-
*
|
|
294
|
+
* Coerces the three inputs the bridge codec refuses back to what upstream
|
|
295
|
+
* Baileys writes, and returns `value` itself when there was nothing to coerce.
|
|
259
296
|
*
|
|
260
297
|
* Reference equality is the signal: the caller only reaches here after an
|
|
261
298
|
* encode threw, and an unchanged result means the failure was something else
|
|
262
|
-
* — a genuinely invalid number, a missing codec — which
|
|
299
|
+
* — a genuinely invalid number, an unknown enum name, a missing codec — which
|
|
300
|
+
* must keep propagating.
|
|
263
301
|
*
|
|
264
302
|
* Copy-on-write throughout, like `projectForEncode`: a branch with nothing to
|
|
265
303
|
* fix is shared, not rebuilt.
|
|
@@ -286,7 +324,16 @@ const repairMessage = (schemaId, value, ancestors) => {
|
|
|
286
324
|
const current = value[field[0]];
|
|
287
325
|
if (current === null || current === undefined)
|
|
288
326
|
continue;
|
|
289
|
-
const repair = (item) =>
|
|
327
|
+
const repair = (item) => {
|
|
328
|
+
if (field[1] === PROTO_FIELD_KIND.message)
|
|
329
|
+
return repairMessage(field[2], item, seen);
|
|
330
|
+
// `?? item`, not `|| item`: 0 is a valid wire value (e.g. `"NONE"`).
|
|
331
|
+
// Unknown names stay strings, so the retry still throws and the
|
|
332
|
+
// original failure keeps propagating instead of becoming a silent 0.
|
|
333
|
+
if (field[1] === PROTO_FIELD_KIND.enum && typeof item === 'string')
|
|
334
|
+
return enumValueFor(field[2], item) ?? item;
|
|
335
|
+
return repairScalar(field[1], item);
|
|
336
|
+
};
|
|
290
337
|
let converted = current;
|
|
291
338
|
if (field[3] & PROTO_FIELD_FLAG.repeated) {
|
|
292
339
|
if (Array.isArray(current)) {
|
|
@@ -457,8 +504,8 @@ class ProtoCompatibilityRuntime {
|
|
|
457
504
|
throw new Error(`protobuf codec unavailable for ${path}`);
|
|
458
505
|
const projected = this.projectForEncode(schemaId, message);
|
|
459
506
|
const encoded = sourceCodec.encode(projected);
|
|
460
|
-
// The bridge refuses
|
|
461
|
-
// Baileys still encodes
|
|
507
|
+
// The bridge refuses three inputs it used to accept silently, and upstream
|
|
508
|
+
// Baileys still encodes all three. Repairing on failure rather than checking
|
|
462
509
|
// every field on the way in is what keeps the ordinary encode free: a
|
|
463
510
|
// message the codec accepts never reaches the repair, and one that does
|
|
464
511
|
// not was already going to throw.
|
package/lib/Socket/events.js
CHANGED
|
@@ -22,7 +22,7 @@ import { buildGroupCreateStubMessage, buildGroupJoinRequestEvents, buildGroupNot
|
|
|
22
22
|
import { emitMessageUpsert } from '../Compatibility/message-upsert.js';
|
|
23
23
|
import { extractMessageCappingPayload } from './message-capping.js';
|
|
24
24
|
import { mapReachoutTimelock } from './reachout.js';
|
|
25
|
-
import { isReconnectableConnectFailure } from './terminal-close.js';
|
|
25
|
+
import { isReconnectableConnectFailure, mapConnectFailureToDisconnect } from './terminal-close.js';
|
|
26
26
|
const CANONICAL_MESSAGE_EVENT = 'message';
|
|
27
27
|
const MESSAGE_UPSERT_APPEND = 'append';
|
|
28
28
|
const MESSAGE_UPSERT_NOTIFY = 'notify';
|
|
@@ -191,41 +191,6 @@ const emitRetrying = (ctx) => ctx.ev.emit('connection.update', {
|
|
|
191
191
|
* dispatcher for why `badSession` was the wrong home.
|
|
192
192
|
*/
|
|
193
193
|
const CLIENT_OUTDATED_STATUS = 405;
|
|
194
|
-
/**
|
|
195
|
-
* Map bridge `ConnectFailureReason` wire codes (per the bridge's
|
|
196
|
-
* `.d.ts` annotation) onto upstream Baileys' `DisconnectReason`.
|
|
197
|
-
* Unknown codes fall through to `connectionClosed` so existing
|
|
198
|
-
* reconnect heuristics keep working.
|
|
199
|
-
*
|
|
200
|
-
* Several cases here are belt-and-braces: the engine dispatches its own event
|
|
201
|
-
* for `is_logged_out()` reasons (401/403/406) and for 405, so those never
|
|
202
|
-
* reach `connectFailure` in practice. Kept because they cost nothing and the
|
|
203
|
-
* engine's routing is not ours to depend on.
|
|
204
|
-
*/
|
|
205
|
-
const mapConnectFailureToDisconnect = (reason) => {
|
|
206
|
-
switch (reason) {
|
|
207
|
-
case 401: // LoggedOut
|
|
208
|
-
case 403: // MainDeviceGone
|
|
209
|
-
case 406: // UnknownLogout
|
|
210
|
-
return DisconnectReason.loggedOut;
|
|
211
|
-
case 402: // TempBanned
|
|
212
|
-
return DisconnectReason.forbidden;
|
|
213
|
-
case 405: // ClientOutdated
|
|
214
|
-
return CLIENT_OUTDATED_STATUS;
|
|
215
|
-
case 411: // MultideviceMismatch (legacy alias)
|
|
216
|
-
return DisconnectReason.multideviceMismatch;
|
|
217
|
-
case 503: // ServiceUnavailable
|
|
218
|
-
case 501: // Experimental
|
|
219
|
-
return DisconnectReason.unavailableService;
|
|
220
|
-
case 408: // Timed out
|
|
221
|
-
return DisconnectReason.timedOut;
|
|
222
|
-
case 515: // RestartRequired
|
|
223
|
-
return DisconnectReason.restartRequired;
|
|
224
|
-
// 400, 409, 413, 414, 415, 418, 500, undefined → generic close
|
|
225
|
-
default:
|
|
226
|
-
return DisconnectReason.connectionClosed;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
194
|
const describeTempBan = (code) => {
|
|
230
195
|
switch (code) {
|
|
231
196
|
case 101:
|
package/lib/Socket/groups.js
CHANGED
|
@@ -20,6 +20,27 @@ export const JOIN_APPROVAL_MODES = ['on', 'off'];
|
|
|
20
20
|
// exact as a double, and that shape carries no methods. The helper reads both
|
|
21
21
|
// forms, and reconstructs the high word instead of dropping it.
|
|
22
22
|
const inviteExpirationNumber = (value) => toNumber(value);
|
|
23
|
+
// The core's V4 join parser only accepts a `<group>`, `<community>` or
|
|
24
|
+
// `<membership_approval_request>` child, but the server also answers a
|
|
25
|
+
// successful join with a bare `<iq type="result">` (WA Web's own
|
|
26
|
+
// `AcceptGroupAddResponseSuccess` variant requires no child at all — only the
|
|
27
|
+
// result envelope whose `from` echoes the request's `to`). The core reports
|
|
28
|
+
// that shape as an `IqError::ParseError`, which the bridge surfaces as
|
|
29
|
+
// `kind: 'internal'`. Error stanzas never reach the parser (they become
|
|
30
|
+
// `kind: 'server'` one layer below), so this substring can only mean the join
|
|
31
|
+
// was accepted and the JID carrier is missing — never a rejection.
|
|
32
|
+
const BARE_JOIN_SUCCESS_FRAGMENT = 'expected <group>, <community>, or <membership_approval_request> in join response';
|
|
33
|
+
// A bare `<iq type="result">` join success, as described above. Anything else
|
|
34
|
+
// (server rejections, timeouts, transport loss, protocol violations) must keep
|
|
35
|
+
// propagating.
|
|
36
|
+
const isBareJoinSuccess = (error) => {
|
|
37
|
+
if (!(error instanceof Error) || error.name !== 'WhatsAppError')
|
|
38
|
+
return false;
|
|
39
|
+
const kind = error.kind;
|
|
40
|
+
if (kind !== 'internal')
|
|
41
|
+
return false;
|
|
42
|
+
return typeof error.message === 'string' && error.message.includes(BARE_JOIN_SUCCESS_FRAGMENT);
|
|
43
|
+
};
|
|
23
44
|
export const makeGroupMethods = (ctx) => {
|
|
24
45
|
const groupMetadata = async (jid) => {
|
|
25
46
|
const metadata = await (await ctx.getClient()).getGroupMetadata(jid);
|
|
@@ -34,10 +55,22 @@ export const makeGroupMethods = (ctx) => {
|
|
|
34
55
|
// oxlint-disable-next-line typescript/no-explicit-any -- the established public contract returns Promise<any>.
|
|
35
56
|
) => {
|
|
36
57
|
const messageKey = typeof key === 'string' ? { remoteJid: key } : key;
|
|
37
|
-
|
|
58
|
+
const groupJid = inviteMessage.groupJid;
|
|
59
|
+
if (!groupJid || !inviteMessage.inviteCode || !messageKey.remoteJid) {
|
|
38
60
|
throw new TypeError('groupAcceptInviteV4 requires groupJid, inviteCode and inviter JID');
|
|
39
61
|
}
|
|
40
|
-
|
|
62
|
+
let joinedJid;
|
|
63
|
+
try {
|
|
64
|
+
joinedJid = await (await ctx.getClient()).groupAcceptInviteV4(groupJid, inviteMessage.inviteCode, inviteExpirationNumber(inviteMessage.inviteExpiration), messageKey.remoteJid);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// The join was accepted but the response carried no JID node.
|
|
68
|
+
// Baileys returns the envelope's `from` here, which echoes the
|
|
69
|
+
// request's `to` — the group JID we already hold.
|
|
70
|
+
if (!isBareJoinSuccess(error))
|
|
71
|
+
throw error;
|
|
72
|
+
joinedJid = groupJid;
|
|
73
|
+
}
|
|
41
74
|
if (messageKey.id) {
|
|
42
75
|
const expiredInvite = proto.Message.GroupInviteMessage.fromObject(inviteMessage);
|
|
43
76
|
expiredInvite.inviteExpiration = 0;
|