@oxidezap/baileyrs 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/lib/Bridge/primitives.d.ts +49 -3
- package/lib/Bridge/primitives.js +126 -8
- package/lib/Bridge/schema.js +208 -87
- package/lib/Bridge/types.d.ts +61 -2
- package/lib/Compatibility/derived-stanza-nodes.d.ts +28 -0
- package/lib/Compatibility/derived-stanza-nodes.js +71 -0
- package/lib/Compatibility/encode-proto.d.ts +17 -0
- package/lib/Compatibility/encode-proto.js +32 -0
- package/lib/Compatibility/proto-runtime.d.ts +10 -0
- package/lib/Compatibility/proto-runtime.js +176 -11
- package/lib/Socket/events.js +35 -6
- package/lib/Socket/groups.js +6 -2
- package/lib/Socket/index.js +4 -3
- package/lib/Socket/messages.js +12 -6
- package/lib/Types/Auth.d.ts +4 -1
- package/lib/Types/Events.d.ts +17 -0
- package/lib/Types/Message.d.ts +17 -1
- package/lib/Utils/event-buffer.js +48 -11
- package/lib/Utils/messages.js +30 -7
- package/lib/Utils/process-history-message.d.ts +11 -2
- package/lib/Utils/process-history-message.js +11 -7
- package/lib/Utils/use-multi-file-auth-state.js +45 -0
- package/package.json +9 -3
package/lib/Bridge/schema.js
CHANGED
|
@@ -24,14 +24,17 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { processHistoryMessage } from '../Utils/process-history-message.js';
|
|
26
26
|
import { isJidGroup } from '../WABinary/jid-utils.js';
|
|
27
|
-
import { asBoolOr, asJidAddressString, asJidString, asNumber, asString, isObject, normalizeDiscriminator, toUnixSeconds } from './primitives.js';
|
|
27
|
+
import { absoluteFromDuration, asBool, asBoolOr, asStringArray, asDurationSeconds, asInt64, asJidAddressString, asJidString, asNumber, asString, asUnixSeconds, isObject, normalizeDiscriminator, toUnixSeconds } 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
|
|
31
31
|
* the unexported `PinAction` / `MuteAction` (resolves to `any`). Narrow
|
|
32
32
|
* once at the call site.
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
// Tolerates a missing `data` slot: the adapter table has to be total against
|
|
35
|
+
// whatever the runtime sends, and an entry that reads the action before any
|
|
36
|
+
// other guard would otherwise throw rather than drop (`bridge:adapt-total`).
|
|
37
|
+
const extractAction = (data) => isObject(data?.action) ? data.action : undefined;
|
|
35
38
|
/** A group JID is authoritative when an older producer leaves `is_group` false. */
|
|
36
39
|
const resolveIsGroup = (wireValue, chatJid) => asBoolOr(wireValue, false) || isJidGroup(chatJid) === true;
|
|
37
40
|
const resolveParticipantAlt = (senderAlt, isGroup) => isGroup ? senderAlt : undefined;
|
|
@@ -60,7 +63,12 @@ const ADAPTERS = {
|
|
|
60
63
|
temporary_ban: data => ({
|
|
61
64
|
type: 'temporaryBan',
|
|
62
65
|
code: asNumber(data?.code),
|
|
63
|
-
|
|
66
|
+
// The wire sends how long the ban lasts, and WA Web renders it that way
|
|
67
|
+
// ("you'll be able to use WhatsApp again in {duration}"). The canonical
|
|
68
|
+
// event promises the instant it lifts, which is what the socket formats
|
|
69
|
+
// and what a reconnect delay subtracts `Date.now()` from — so the
|
|
70
|
+
// conversion happens here, once, rather than in each of them.
|
|
71
|
+
expire: absoluteFromDuration(asDurationSeconds(data?.expire))
|
|
64
72
|
}),
|
|
65
73
|
qr_scanned_without_multidevice: () => ({ type: 'qrScannedWithoutMultidevice' }),
|
|
66
74
|
logged_out: data => ({
|
|
@@ -68,51 +76,51 @@ const ADAPTERS = {
|
|
|
68
76
|
onConnect: asBoolOr(data?.on_connect, false),
|
|
69
77
|
reason: asString(data?.reason)
|
|
70
78
|
}),
|
|
71
|
-
qr: data => (data
|
|
72
|
-
pairing_code: data => (data
|
|
79
|
+
qr: data => (data?.code ? { type: 'qr', code: data?.code } : null),
|
|
80
|
+
pairing_code: data => (data?.code ? { type: 'qr', code: data?.code } : null),
|
|
73
81
|
pairing_code_refresh: data => ({
|
|
74
82
|
type: 'noop',
|
|
75
83
|
bridgeType: 'pairing_code_refresh',
|
|
76
|
-
detail: data
|
|
84
|
+
detail: data?.force_manual ? 'force_manual' : 'automatic'
|
|
77
85
|
}),
|
|
78
86
|
pair_passkey_request: () => ({ type: 'noop', bridgeType: 'pair_passkey_request' }),
|
|
79
87
|
pair_passkey_confirmation: data => ({
|
|
80
88
|
type: 'noop',
|
|
81
89
|
bridgeType: 'pair_passkey_confirmation',
|
|
82
|
-
detail: data
|
|
90
|
+
detail: data?.skip_handoff_ux ? 'handoff_verified' : 'confirmation_required'
|
|
83
91
|
}),
|
|
84
92
|
pair_passkey_error: data => ({
|
|
85
93
|
type: 'noop',
|
|
86
94
|
bridgeType: 'pair_passkey_error',
|
|
87
|
-
detail: asString(data
|
|
95
|
+
detail: asString(data?.error)
|
|
88
96
|
}),
|
|
89
97
|
pair_success: data => {
|
|
90
98
|
// `id` and `lid` come typed as `Jid` in the bridge .d.ts, but the
|
|
91
99
|
// bridge actually serializes pair_success.{id,lid} as strings (see
|
|
92
100
|
// the wire log) — accept both shapes.
|
|
93
|
-
const id = typeof data
|
|
101
|
+
const id = typeof data?.id === 'string' ? data?.id : asJidString(data?.id);
|
|
94
102
|
if (!id)
|
|
95
103
|
return null;
|
|
96
104
|
return {
|
|
97
105
|
type: 'pairSuccess',
|
|
98
106
|
id,
|
|
99
|
-
lid: typeof data
|
|
100
|
-
platform: asString(data
|
|
101
|
-
businessName: asString(data
|
|
107
|
+
lid: typeof data?.lid === 'string' ? data?.lid : asJidString(data?.lid),
|
|
108
|
+
platform: asString(data?.platform),
|
|
109
|
+
businessName: asString(data?.business_name)
|
|
102
110
|
};
|
|
103
111
|
},
|
|
104
112
|
pair_error: data => ({
|
|
105
113
|
type: 'pairError',
|
|
106
|
-
error: asString(data
|
|
107
|
-
id: typeof data
|
|
108
|
-
lid: typeof data
|
|
109
|
-
businessName: asString(data
|
|
110
|
-
platform: asString(data
|
|
114
|
+
error: asString(data?.error) ?? 'Unknown pairing error',
|
|
115
|
+
id: typeof data?.id === 'string' ? data?.id : asJidString(data?.id),
|
|
116
|
+
lid: typeof data?.lid === 'string' ? data?.lid : asJidString(data?.lid),
|
|
117
|
+
businessName: asString(data?.business_name),
|
|
118
|
+
platform: asString(data?.platform)
|
|
111
119
|
}),
|
|
112
120
|
connect_failure: data => isObject(data)
|
|
113
|
-
? { type: 'connectFailure', message: asString(data
|
|
121
|
+
? { type: 'connectFailure', message: asString(data?.message), reason: asNumber(data?.reason) }
|
|
114
122
|
: { type: 'connectFailure' },
|
|
115
|
-
stream_error: data => ({ type: 'streamError', code: asString(data
|
|
123
|
+
stream_error: data => ({ type: 'streamError', code: asString(data?.code) ?? 'unknown' }),
|
|
116
124
|
// ── Messages ──
|
|
117
125
|
message: (data, logger) => adaptMessage(data, logger),
|
|
118
126
|
receipt: (data, logger) => adaptReceipt(data, logger),
|
|
@@ -121,16 +129,16 @@ const ADAPTERS = {
|
|
|
121
129
|
// while NACKs become `messages.update` with status ERROR. Preserve every
|
|
122
130
|
// typed field here so the dispatcher can reproduce both surfaces.
|
|
123
131
|
server_ack: data => {
|
|
124
|
-
const id = asString(data
|
|
132
|
+
const id = asString(data?.id);
|
|
125
133
|
if (!id)
|
|
126
134
|
return null;
|
|
127
135
|
return {
|
|
128
136
|
type: 'serverAck',
|
|
129
137
|
id,
|
|
130
|
-
class: asString(data
|
|
131
|
-
from: asJidString(data
|
|
132
|
-
timestamp:
|
|
133
|
-
error: asString(data
|
|
138
|
+
class: asString(data?.class),
|
|
139
|
+
from: asJidString(data?.from),
|
|
140
|
+
timestamp: asUnixSeconds(data?.timestamp),
|
|
141
|
+
error: asString(data?.error)
|
|
134
142
|
};
|
|
135
143
|
},
|
|
136
144
|
undecryptable_message: data => {
|
|
@@ -141,7 +149,7 @@ const ADAPTERS = {
|
|
|
141
149
|
// `messages-recv.ts:1352`.
|
|
142
150
|
if (!isObject(data))
|
|
143
151
|
return null;
|
|
144
|
-
const info = isObject(data
|
|
152
|
+
const info = isObject(data?.info) ? data?.info : undefined;
|
|
145
153
|
if (!info)
|
|
146
154
|
return null;
|
|
147
155
|
const src = isObject(info.source) ? info.source : undefined;
|
|
@@ -164,45 +172,45 @@ const ADAPTERS = {
|
|
|
164
172
|
pushName: asString(info.push_name),
|
|
165
173
|
participantAlt: resolveParticipantAlt(senderAlt, isGroup),
|
|
166
174
|
remoteJidAlt: resolveRemoteJidAlt(senderAlt, recipientAlt, isGroup, isFromMe),
|
|
167
|
-
isUnavailable: asBoolOr(data
|
|
168
|
-
unavailableType: asString(data
|
|
169
|
-
decryptFailMode: asString(data
|
|
175
|
+
isUnavailable: asBoolOr(data?.is_unavailable, false),
|
|
176
|
+
unavailableType: asString(data?.unavailable_type),
|
|
177
|
+
decryptFailMode: asString(data?.decrypt_fail_mode),
|
|
170
178
|
raw: data
|
|
171
179
|
};
|
|
172
180
|
},
|
|
173
181
|
// ── Contacts ──
|
|
174
182
|
push_name_update: data => {
|
|
175
|
-
const jid = asJidString(data
|
|
176
|
-
return jid ? { type: 'pushNameUpdate', jid, newPushName: asString(data
|
|
183
|
+
const jid = asJidString(data?.jid);
|
|
184
|
+
return jid ? { type: 'pushNameUpdate', jid, newPushName: asString(data?.new_push_name) } : null;
|
|
177
185
|
},
|
|
178
186
|
contact_update: data => adaptContactUpdate(data),
|
|
179
187
|
contact_updated: data => adaptContactUpdate(data),
|
|
180
188
|
picture_update: data => {
|
|
181
|
-
const jid = asJidString(data
|
|
189
|
+
const jid = asJidString(data?.jid);
|
|
182
190
|
if (!jid)
|
|
183
191
|
return null;
|
|
184
192
|
return {
|
|
185
193
|
type: 'pictureUpdate',
|
|
186
194
|
jid,
|
|
187
|
-
removed: asBoolOr(data
|
|
188
|
-
author: asJidString(data
|
|
189
|
-
pictureId: asString(data
|
|
195
|
+
removed: asBoolOr(data?.removed, false),
|
|
196
|
+
author: asJidString(data?.author),
|
|
197
|
+
pictureId: asString(data?.picture_id)
|
|
190
198
|
};
|
|
191
199
|
},
|
|
192
200
|
// ── Presence ──
|
|
193
201
|
presence: data => {
|
|
194
|
-
const from = asJidString(data
|
|
202
|
+
const from = asJidString(data?.from);
|
|
195
203
|
if (!from)
|
|
196
204
|
return null;
|
|
197
205
|
return {
|
|
198
206
|
type: 'presence',
|
|
199
207
|
from,
|
|
200
|
-
unavailable: asBoolOr(data
|
|
201
|
-
lastSeen:
|
|
208
|
+
unavailable: asBoolOr(data?.unavailable, false),
|
|
209
|
+
lastSeen: asUnixSeconds(data?.last_seen)
|
|
202
210
|
};
|
|
203
211
|
},
|
|
204
212
|
chat_presence: data => {
|
|
205
|
-
const src = isObject(data
|
|
213
|
+
const src = isObject(data?.source) ? data?.source : undefined;
|
|
206
214
|
if (!src)
|
|
207
215
|
return null;
|
|
208
216
|
const chat = asJidString(src.chat);
|
|
@@ -211,11 +219,11 @@ const ADAPTERS = {
|
|
|
211
219
|
return null;
|
|
212
220
|
// Bridge sends `media: ''` for "no media" — normalize to undefined
|
|
213
221
|
// so consumers can rely on field omission as the absence signal.
|
|
214
|
-
const media = asString(data
|
|
222
|
+
const media = asString(data?.media);
|
|
215
223
|
// Bridge `state` MUST be one of the two canonical values; defaulting
|
|
216
224
|
// to 'composing' on an unknown/missing value would synthesize a
|
|
217
225
|
// false typing indicator. Drop the event instead.
|
|
218
|
-
const rawState = asString(data
|
|
226
|
+
const rawState = asString(data?.state);
|
|
219
227
|
if (rawState !== 'composing' && rawState !== 'paused')
|
|
220
228
|
return null;
|
|
221
229
|
return {
|
|
@@ -230,44 +238,44 @@ const ADAPTERS = {
|
|
|
230
238
|
group_update: (data, logger) => adaptGroupUpdate(data, logger),
|
|
231
239
|
// ── Chat state ──
|
|
232
240
|
archive_update: data => {
|
|
233
|
-
const jid = asJidString(data
|
|
241
|
+
const jid = asJidString(data?.jid);
|
|
234
242
|
if (!jid)
|
|
235
243
|
return null;
|
|
236
244
|
return { type: 'archiveUpdate', jid, archived: asBoolOr(extractAction(data)?.archived, true) };
|
|
237
245
|
},
|
|
238
246
|
pin_update: data => {
|
|
239
|
-
const jid = asJidString(data
|
|
247
|
+
const jid = asJidString(data?.jid);
|
|
240
248
|
if (!jid)
|
|
241
249
|
return null;
|
|
242
250
|
return {
|
|
243
251
|
type: 'pinUpdate',
|
|
244
252
|
jid,
|
|
245
|
-
timestamp:
|
|
253
|
+
timestamp: asUnixSeconds(data?.timestamp),
|
|
246
254
|
pinned: asBoolOr(extractAction(data)?.pinned, true)
|
|
247
255
|
};
|
|
248
256
|
},
|
|
249
257
|
mute_update: data => {
|
|
250
|
-
const jid = asJidString(data
|
|
258
|
+
const jid = asJidString(data?.jid);
|
|
251
259
|
if (!jid)
|
|
252
260
|
return null;
|
|
253
261
|
const action = extractAction(data);
|
|
254
262
|
return {
|
|
255
263
|
type: 'muteUpdate',
|
|
256
264
|
jid,
|
|
257
|
-
timestamp:
|
|
265
|
+
timestamp: asUnixSeconds(data?.timestamp),
|
|
258
266
|
muted: asBoolOr(action?.muted, true),
|
|
259
|
-
muteEndTimestamp:
|
|
267
|
+
muteEndTimestamp: asInt64(action?.muteEndTimestamp) ?? asInt64(action?.mute_end_timestamp)
|
|
260
268
|
};
|
|
261
269
|
},
|
|
262
270
|
star_update: data => adaptStarUpdate(data),
|
|
263
271
|
mark_chat_as_read_update: data => {
|
|
264
|
-
const jid = asJidString(data
|
|
272
|
+
const jid = asJidString(data?.jid);
|
|
265
273
|
if (!jid)
|
|
266
274
|
return null;
|
|
267
275
|
return { type: 'markChatAsReadUpdate', jid, read: asBoolOr(extractAction(data)?.read, true) };
|
|
268
276
|
},
|
|
269
277
|
label_edit_update: data => {
|
|
270
|
-
const labelId = asString(data
|
|
278
|
+
const labelId = asString(data?.label_id);
|
|
271
279
|
if (!labelId)
|
|
272
280
|
return { type: 'noop', bridgeType: 'label_edit_update' };
|
|
273
281
|
const action = extractAction(data);
|
|
@@ -284,39 +292,128 @@ const ADAPTERS = {
|
|
|
284
292
|
};
|
|
285
293
|
},
|
|
286
294
|
label_association_update: data => {
|
|
287
|
-
const labelId = asString(data
|
|
288
|
-
const chatJid = asJidString(data
|
|
295
|
+
const labelId = asString(data?.label_id);
|
|
296
|
+
const chatJid = asJidString(data?.chat_jid);
|
|
289
297
|
if (!labelId || !chatJid)
|
|
290
298
|
return { type: 'noop', bridgeType: 'label_association_update' };
|
|
291
299
|
// `action.labeled === true` → label added to the chat, else removed.
|
|
292
300
|
return { type: 'labelAssociation', labelId, chatJid, labeled: asBoolOr(extractAction(data)?.labeled, true) };
|
|
293
301
|
},
|
|
302
|
+
/**
|
|
303
|
+
* The per-message half of `labels.association`, which used to have no path.
|
|
304
|
+
* Same canonical event as the chat one, told apart by carrying a message.
|
|
305
|
+
*/
|
|
306
|
+
message_label_association_update: data => {
|
|
307
|
+
const labelId = asString(data?.label_id);
|
|
308
|
+
const chatJid = asJidString(data?.chat_jid);
|
|
309
|
+
const messageId = asString(data?.message_id);
|
|
310
|
+
if (!labelId || !chatJid || !messageId) {
|
|
311
|
+
return { type: 'noop', bridgeType: 'message_label_association_update' };
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
type: 'labelAssociation',
|
|
315
|
+
labelId,
|
|
316
|
+
chatJid,
|
|
317
|
+
messageId,
|
|
318
|
+
labeled: asBoolOr(extractAction(data)?.labeled, true)
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* What a degraded app-state sync left behind. The engine announces the
|
|
323
|
+
* connection anyway, so without this a consumer is told a session with no
|
|
324
|
+
* push name is healthy and has nothing to read that says otherwise.
|
|
325
|
+
*/
|
|
326
|
+
app_state_sync_failed: data => ({
|
|
327
|
+
type: 'appStateSyncFailed',
|
|
328
|
+
fatal: asStringArray(data?.fatal),
|
|
329
|
+
retryable: asStringArray(data?.retryable),
|
|
330
|
+
skipped: asStringArray(data?.skipped),
|
|
331
|
+
connected: asBoolOr(data?.connected, false)
|
|
332
|
+
}),
|
|
333
|
+
/**
|
|
334
|
+
* The QR refs ran out. Upstream ends the socket with `timedOut` when its own
|
|
335
|
+
* QR timer gives up (`Socket/socket.ts`), which is the same end state, so
|
|
336
|
+
* this becomes the same terminal close rather than a new signal to learn.
|
|
337
|
+
*/
|
|
338
|
+
pairing_qr_codes_exhausted: () => ({ type: 'qrCodesExhausted' }),
|
|
339
|
+
/**
|
|
340
|
+
* Another linked device turned link previews on or off account-wide.
|
|
341
|
+
*
|
|
342
|
+
* Upstream carries this on `settings.update`, reached through app state
|
|
343
|
+
* (`Utils/chat-utils.ts` branches on `privacySettingDisableLinkPreviewsAction`
|
|
344
|
+
* and emits the action as the value). Same event, different pipe — so the
|
|
345
|
+
* value is the action itself, not the decoded flag.
|
|
346
|
+
*
|
|
347
|
+
* `previews_disabled` is that flag already decoded by the bridge, which is
|
|
348
|
+
* what fills the action in when the payload carried the flag alone. The
|
|
349
|
+
* bridge only emits this event when the wire carried the flag, so the last
|
|
350
|
+
* fallback is unreachable in practice and exists so the value always has
|
|
351
|
+
* the field upstream's consumers read.
|
|
352
|
+
*/
|
|
353
|
+
disable_link_previews_update: data => {
|
|
354
|
+
const action = extractAction(data);
|
|
355
|
+
return {
|
|
356
|
+
type: 'settingUpdate',
|
|
357
|
+
setting: 'disableLinkPreviews',
|
|
358
|
+
value: {
|
|
359
|
+
...action,
|
|
360
|
+
isPreviewsDisabled: asBool(action?.isPreviewsDisabled) ?? asBoolOr(data?.previews_disabled, false)
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
/**
|
|
365
|
+
* A pair-code request failed, so any code the user was shown is spent.
|
|
366
|
+
*
|
|
367
|
+
* Same lifecycle as `pair_error`, which is why it adapts to the same
|
|
368
|
+
* canonical event: the socket lives on and the engine takes another
|
|
369
|
+
* request, so this must not read as a close, and the code on screen has to
|
|
370
|
+
* stop being offered. `pairError` is the handler that does both — a
|
|
371
|
+
* pairing code surfaces as `qr` (see `pairing_code` above), and `connecting`
|
|
372
|
+
* clears it while saying a fresh one can be asked for.
|
|
373
|
+
*
|
|
374
|
+
* `rejection` and `backoff` ride along for the log. Neither changes what a
|
|
375
|
+
* consumer does here, and the engine owns the retry — but a throttle the
|
|
376
|
+
* server named itself is the difference between a code that will come back
|
|
377
|
+
* and one that will not, and dropping it leaves that unexplained.
|
|
378
|
+
*/
|
|
379
|
+
pairing_code_error: data => ({
|
|
380
|
+
type: 'pairError',
|
|
381
|
+
error: asString(data?.error) ?? 'pairing code rejected',
|
|
382
|
+
rejection: asNumber(data?.rejection),
|
|
383
|
+
backoff: asNumber(data?.backoff)
|
|
384
|
+
}),
|
|
385
|
+
// Acknowledged with no Baileys equivalent: upstream has no channel for a
|
|
386
|
+
// contact deletion (`contacts.update` only upserts), for quick replies, or
|
|
387
|
+
// for a call placed on the phone.
|
|
388
|
+
contact_removed: () => ({ type: 'noop', bridgeType: 'contact_removed' }),
|
|
389
|
+
quick_reply_update: () => ({ type: 'noop', bridgeType: 'quick_reply_update' }),
|
|
390
|
+
call_log_sync: () => ({ type: 'noop', bridgeType: 'call_log_sync' }),
|
|
294
391
|
// ── Calls ──
|
|
295
392
|
incoming_call: (data, logger) => adaptIncomingCall(data, logger),
|
|
296
393
|
missed_call: data => {
|
|
297
|
-
const from = asJidString(data
|
|
298
|
-
const callId = asString(data
|
|
394
|
+
const from = asJidString(data?.from);
|
|
395
|
+
const callId = asString(data?.call_id);
|
|
299
396
|
if (!from || !callId)
|
|
300
397
|
return null;
|
|
301
398
|
return {
|
|
302
399
|
type: 'incomingCall',
|
|
303
400
|
from,
|
|
304
|
-
timestamp: toUnixSeconds(data
|
|
305
|
-
offline: data
|
|
401
|
+
timestamp: toUnixSeconds(data?.timestamp),
|
|
402
|
+
offline: data?.reason === 'offline',
|
|
306
403
|
action: { type: 'timeout', callId }
|
|
307
404
|
};
|
|
308
405
|
},
|
|
309
406
|
call_ended_elsewhere: data => {
|
|
310
|
-
const from = asJidString(data
|
|
311
|
-
const callId = asString(data
|
|
407
|
+
const from = asJidString(data?.from);
|
|
408
|
+
const callId = asString(data?.call_id);
|
|
312
409
|
if (!from || !callId)
|
|
313
410
|
return null;
|
|
314
411
|
return {
|
|
315
412
|
type: 'incomingCall',
|
|
316
413
|
from,
|
|
317
|
-
timestamp: toUnixSeconds(data
|
|
414
|
+
timestamp: toUnixSeconds(data?.timestamp),
|
|
318
415
|
offline: false,
|
|
319
|
-
action: { type: data
|
|
416
|
+
action: { type: data?.outcome === 'accepted' ? 'accept' : 'reject', callId }
|
|
320
417
|
};
|
|
321
418
|
},
|
|
322
419
|
// ── History sync — fully decoded by the bridge, normalized 1:1 with upstream ──
|
|
@@ -361,35 +458,35 @@ const ADAPTERS = {
|
|
|
361
458
|
self_push_name_updated: () => ({ type: 'noop', bridgeType: 'self_push_name_updated' }),
|
|
362
459
|
offline_sync_completed: data => ({
|
|
363
460
|
type: 'offlineSyncCompleted',
|
|
364
|
-
count: asNumber(data
|
|
461
|
+
count: asNumber(data?.count) ?? 0
|
|
365
462
|
}),
|
|
366
463
|
offline_sync_preview: () => ({ type: 'noop', bridgeType: 'offline_sync_preview' }),
|
|
367
464
|
dirty_state: data => {
|
|
368
|
-
const dirtyType = asString(data
|
|
465
|
+
const dirtyType = asString(data?.dirty_type);
|
|
369
466
|
if (!dirtyType)
|
|
370
467
|
return null;
|
|
371
|
-
return { type: 'dirtyState', dirtyType, timestamp: asNumber(data
|
|
468
|
+
return { type: 'dirtyState', dirtyType, timestamp: asNumber(data?.timestamp) };
|
|
372
469
|
},
|
|
373
470
|
device_list_update: () => ({ type: 'noop', bridgeType: 'device_list_update' }),
|
|
374
471
|
identity_change: () => ({ type: 'noop', bridgeType: 'identity_change' }),
|
|
375
472
|
disappearing_mode_changed: data => {
|
|
376
|
-
const jid = asJidString(data
|
|
377
|
-
const duration = asNumber(data
|
|
473
|
+
const jid = asJidString(data?.from);
|
|
474
|
+
const duration = asNumber(data?.duration);
|
|
378
475
|
if (!jid || duration == null)
|
|
379
476
|
return { type: 'noop', bridgeType: 'disappearing_mode_changed' };
|
|
380
477
|
return {
|
|
381
478
|
type: 'disappearingModeChanged',
|
|
382
479
|
jid,
|
|
383
480
|
duration,
|
|
384
|
-
settingTimestamp: asNumber(data
|
|
481
|
+
settingTimestamp: asNumber(data?.setting_timestamp)
|
|
385
482
|
};
|
|
386
483
|
},
|
|
387
484
|
business_status_update: () => ({ type: 'noop', bridgeType: 'business_status_update' }),
|
|
388
485
|
newsletter_live_update: data => {
|
|
389
|
-
const newsletterJid = asJidString(data
|
|
486
|
+
const newsletterJid = asJidString(data?.newsletter_jid);
|
|
390
487
|
if (!newsletterJid)
|
|
391
488
|
return { type: 'noop', bridgeType: 'newsletter_live_update' };
|
|
392
|
-
const rawMessages = Array.isArray(data
|
|
489
|
+
const rawMessages = Array.isArray(data?.messages) ? data?.messages : [];
|
|
393
490
|
const messages = rawMessages
|
|
394
491
|
.map(m => {
|
|
395
492
|
if (!isObject(m))
|
|
@@ -422,10 +519,10 @@ const ADAPTERS = {
|
|
|
422
519
|
// (old_lid, old_jid) and (new_lid, new_jid). We learn whatever's
|
|
423
520
|
// present and let the dispatcher fan out one upstream event per
|
|
424
521
|
// pair. Mirrors upstream `messages-recv.ts:287`.
|
|
425
|
-
const oldJid = asJidString(data
|
|
426
|
-
const newJid = asJidString(data
|
|
427
|
-
const oldLid = asJidString(data
|
|
428
|
-
const newLid = asJidString(data
|
|
522
|
+
const oldJid = asJidString(data?.old_jid);
|
|
523
|
+
const newJid = asJidString(data?.new_jid);
|
|
524
|
+
const oldLid = asJidString(data?.old_lid);
|
|
525
|
+
const newLid = asJidString(data?.new_lid);
|
|
429
526
|
const mappings = [];
|
|
430
527
|
if (oldLid && oldJid)
|
|
431
528
|
mappings.push({ lid: oldLid, pn: oldJid });
|
|
@@ -438,30 +535,30 @@ const ADAPTERS = {
|
|
|
438
535
|
contact_sync_requested: () => ({ type: 'noop', bridgeType: 'contact_sync_requested' }),
|
|
439
536
|
user_about_update: () => ({ type: 'noop', bridgeType: 'user_about_update' }),
|
|
440
537
|
delete_chat_update: data => {
|
|
441
|
-
const jid = asJidString(data
|
|
538
|
+
const jid = asJidString(data?.jid);
|
|
442
539
|
return jid ? { type: 'chatDelete', jid } : { type: 'noop', bridgeType: 'delete_chat_update' };
|
|
443
540
|
},
|
|
444
541
|
clear_chat_update: data => {
|
|
445
542
|
// Clear = drop all messages but keep the chat. Maps to upstream
|
|
446
543
|
// `messages.delete` `{ jid, all: true }` (the chat-clear surface noted in
|
|
447
544
|
// the messageDelete dispatcher), distinct from chatDelete (whole chat gone).
|
|
448
|
-
const jid = asJidString(data
|
|
545
|
+
const jid = asJidString(data?.jid);
|
|
449
546
|
return jid ? { type: 'chatClear', jid } : { type: 'noop', bridgeType: 'clear_chat_update' };
|
|
450
547
|
},
|
|
451
548
|
// Muting a contact's status (stories) updates. Forwarded for surface completeness,
|
|
452
549
|
// but noop'd: upstream Baileys has no status-mute event/chatModify to map it onto.
|
|
453
550
|
user_status_mute_update: () => ({ type: 'noop', bridgeType: 'user_status_mute_update' }),
|
|
454
551
|
delete_message_for_me_update: data => {
|
|
455
|
-
const chatJid = asJidString(data
|
|
456
|
-
const messageId = asString(data
|
|
552
|
+
const chatJid = asJidString(data?.chat_jid);
|
|
553
|
+
const messageId = asString(data?.message_id);
|
|
457
554
|
if (!chatJid || !messageId)
|
|
458
555
|
return { type: 'noop', bridgeType: 'delete_message_for_me_update' };
|
|
459
556
|
return {
|
|
460
557
|
type: 'messageDelete',
|
|
461
558
|
chatJid,
|
|
462
559
|
messageId,
|
|
463
|
-
fromMe: asBoolOr(data
|
|
464
|
-
participantJid: asJidString(data
|
|
560
|
+
fromMe: asBoolOr(data?.from_me, false),
|
|
561
|
+
participantJid: asJidString(data?.participant_jid)
|
|
465
562
|
};
|
|
466
563
|
},
|
|
467
564
|
// ── Generic notification / raw node ──
|
|
@@ -474,8 +571,8 @@ const ADAPTERS = {
|
|
|
474
571
|
// CanonicalNotification.attrs (typed `Record<string, string>`).
|
|
475
572
|
// Drop non-string values, coerce the rest.
|
|
476
573
|
const attrs = {};
|
|
477
|
-
if (isObject(data
|
|
478
|
-
for (const [k, v] of Object.entries(data
|
|
574
|
+
if (isObject(data?.attrs)) {
|
|
575
|
+
for (const [k, v] of Object.entries(data?.attrs)) {
|
|
479
576
|
if (typeof v === 'string')
|
|
480
577
|
attrs[k] = v;
|
|
481
578
|
else if (typeof v === 'number' || typeof v === 'boolean')
|
|
@@ -484,26 +581,26 @@ const ADAPTERS = {
|
|
|
484
581
|
// sense on a flat attrs map anyway.
|
|
485
582
|
}
|
|
486
583
|
}
|
|
487
|
-
return { type: 'notification', tag: asString(data
|
|
584
|
+
return { type: 'notification', tag: asString(data?.tag) ?? 'notification', attrs };
|
|
488
585
|
},
|
|
489
586
|
raw_node: data => {
|
|
490
587
|
// `BinaryNode` is shaped exactly like the bridge payload
|
|
491
588
|
// (`{ tag, attrs, content }`). Minimal sanity check on `tag`.
|
|
492
|
-
if (!isObject(data) || typeof data
|
|
589
|
+
if (!isObject(data) || typeof data?.tag !== 'string')
|
|
493
590
|
return null;
|
|
494
591
|
return { type: 'rawNode', node: data };
|
|
495
592
|
},
|
|
496
593
|
mex_notification: data => {
|
|
497
|
-
const opName = asString(data
|
|
594
|
+
const opName = asString(data?.op_name);
|
|
498
595
|
if (!opName)
|
|
499
596
|
return null;
|
|
500
|
-
const payload = isObject(data
|
|
597
|
+
const payload = isObject(data?.payload) ? data?.payload : {};
|
|
501
598
|
return {
|
|
502
599
|
type: 'mexNotification',
|
|
503
600
|
opName,
|
|
504
|
-
from: asJidString(data
|
|
505
|
-
stanzaId: asString(data
|
|
506
|
-
offline: asBoolOr(data
|
|
601
|
+
from: asJidString(data?.from),
|
|
602
|
+
stanzaId: asString(data?.stanza_id),
|
|
603
|
+
offline: asBoolOr(data?.offline, false),
|
|
507
604
|
payload
|
|
508
605
|
};
|
|
509
606
|
}
|
|
@@ -516,11 +613,35 @@ export const KNOWN_BRIDGE_EVENT_TYPES = new Set(Object.keys(ADAPTERS));
|
|
|
516
613
|
*/
|
|
517
614
|
export const adaptBridgeEventViaSchema = (event, logger) => {
|
|
518
615
|
const typed = event;
|
|
519
|
-
|
|
520
|
-
|
|
616
|
+
// An own property of the table, not anything the prototype chain answers.
|
|
617
|
+
//
|
|
618
|
+
// The type string comes from the runtime, which gets it from the server, so it
|
|
619
|
+
// is untrusted input into a plain-object lookup. `ADAPTERS.constructor` and
|
|
620
|
+
// `ADAPTERS.toString` resolve to inherited functions: they were called and
|
|
621
|
+
// their return values handed on as canonical events — measured, `constructor`
|
|
622
|
+
// produced an object and `toString` a string, neither of which is an event.
|
|
623
|
+
// `__proto__` and `valueOf` reached the other failure mode and threw.
|
|
624
|
+
if (!Object.hasOwn(ADAPTERS, typed.type)) {
|
|
521
625
|
logger?.debug({ eventType: typed.type }, 'unknown bridge event (no canonical mapping)');
|
|
522
626
|
return null;
|
|
523
627
|
}
|
|
628
|
+
const adapter = ADAPTERS[typed.type];
|
|
629
|
+
// The slot is passed through exactly as it arrived, nullish included.
|
|
630
|
+
//
|
|
631
|
+
// The contract these adapters are documented under is "null on unrecoverable
|
|
632
|
+
// shape mismatch, never a throw", and a throw here does not stay local: it
|
|
633
|
+
// propagates into the socket's event dispatch and takes down the whole event
|
|
634
|
+
// loop rather than the one event. 30 of the 58 declared types threw on an
|
|
635
|
+
// event that arrived with no data — `data.code` on `undefined` — so every
|
|
636
|
+
// adapter in the table now reads its slot optionally.
|
|
637
|
+
//
|
|
638
|
+
// Substituting an empty object here instead was tried and reverted: `{}` is a
|
|
639
|
+
// perfectly good object, so it walks straight past the `isObject(data)` guard
|
|
640
|
+
// that several adapters use to decide the payload is unrecoverable.
|
|
641
|
+
// `history_sync` then built an empty final batch and the socket emitted a
|
|
642
|
+
// spurious `messaging-history.set` where it had previously dropped the event.
|
|
643
|
+
// Each adapter owns that decision; the dispatch must not pre-empt it.
|
|
644
|
+
//
|
|
524
645
|
// `data` cast here is the only `as` in the public path — the bridge
|
|
525
646
|
// runtime is the source of truth that the type matches the discriminator.
|
|
526
647
|
return adapter(typed.data, logger);
|
package/lib/Bridge/types.d.ts
CHANGED
|
@@ -37,6 +37,17 @@ export interface CanonicalPairSuccess {
|
|
|
37
37
|
export interface CanonicalPairError {
|
|
38
38
|
type: 'pairError';
|
|
39
39
|
error: string;
|
|
40
|
+
/**
|
|
41
|
+
* The server's own refusal code, when it answered with one.
|
|
42
|
+
*
|
|
43
|
+
* Absent when the failure was local (validation, no connection) or the
|
|
44
|
+
* request went unanswered — nothing was refused, so there is no status.
|
|
45
|
+
* Carried for the log only: the consumer-visible outcome is the same
|
|
46
|
+
* either way, and the engine owns the retry.
|
|
47
|
+
*/
|
|
48
|
+
rejection?: number;
|
|
49
|
+
/** Seconds the server asked the client to wait before asking again. */
|
|
50
|
+
backoff?: number;
|
|
40
51
|
/** Account JID after pairing (may be set even on error). */
|
|
41
52
|
id?: string;
|
|
42
53
|
/** LID for the account. */
|
|
@@ -445,9 +456,57 @@ export interface CanonicalLabelAssociation {
|
|
|
445
456
|
type: 'labelAssociation';
|
|
446
457
|
labelId: string;
|
|
447
458
|
chatJid: string;
|
|
448
|
-
/**
|
|
459
|
+
/**
|
|
460
|
+
* The message the label is on, for a per-message association.
|
|
461
|
+
*
|
|
462
|
+
* Absent means the label is on the chat itself. The two arrive on separate
|
|
463
|
+
* bridge events and upstream carries both on `labels.association`, keyed by
|
|
464
|
+
* the association's own `type`.
|
|
465
|
+
*/
|
|
466
|
+
messageId?: string;
|
|
467
|
+
/** `true` = label added, `false` = removed. */
|
|
449
468
|
labeled: boolean;
|
|
450
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* The server ran out of QR refs before anyone scanned one.
|
|
472
|
+
*
|
|
473
|
+
* Terminal: nothing else is coming on this socket, and the consumer builds a
|
|
474
|
+
* new one to be shown a fresh code.
|
|
475
|
+
*/
|
|
476
|
+
export interface CanonicalQrCodesExhausted {
|
|
477
|
+
type: 'qrCodesExhausted';
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* A batched app-state sync that did not leave every collection synced.
|
|
481
|
+
*
|
|
482
|
+
* Collections are named as they appear on the wire. `fatal` is the one a
|
|
483
|
+
* consumer usually has to act on: the server refused it and asking again gets
|
|
484
|
+
* the same answer. `connected` says whether the session was announced anyway,
|
|
485
|
+
* which is the difference between "degraded but usable" and "still retrying" —
|
|
486
|
+
* the engine connects on a degraded sync rather than withholding the session,
|
|
487
|
+
* so this event is the only thing that says what is missing from it.
|
|
488
|
+
*/
|
|
489
|
+
export interface CanonicalAppStateSyncFailed {
|
|
490
|
+
type: 'appStateSyncFailed';
|
|
491
|
+
fatal: string[];
|
|
492
|
+
retryable: string[];
|
|
493
|
+
skipped: string[];
|
|
494
|
+
connected: boolean;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* An account-wide setting another linked device changed.
|
|
498
|
+
*
|
|
499
|
+
* Upstream reaches the same place through app state — `Utils/chat-utils.ts`
|
|
500
|
+
* emits `settings.update` for a `privacySettingDisableLinkPreviewsAction` —
|
|
501
|
+
* so this is that channel's payload arriving over a different pipe, not a
|
|
502
|
+
* new contract. The shape is the event's rather than one setting's: another
|
|
503
|
+
* setting the bridge starts reporting is a new arm here, not a new event.
|
|
504
|
+
*/
|
|
505
|
+
export interface CanonicalSettingUpdate {
|
|
506
|
+
type: 'settingUpdate';
|
|
507
|
+
setting: 'disableLinkPreviews';
|
|
508
|
+
value: proto.SyncActionValue.IPrivacySettingDisableLinkPreviewsAction;
|
|
509
|
+
}
|
|
451
510
|
export type CanonicalCallActionType = 'offer' | 'preaccept' | 'transport' | 'relaylatency' | 'accept' | 'reject' | 'terminate' | 'timeout';
|
|
452
511
|
export interface CanonicalCallAction {
|
|
453
512
|
type: CanonicalCallActionType;
|
|
@@ -654,5 +713,5 @@ export interface CanonicalServerAck {
|
|
|
654
713
|
timestamp?: number;
|
|
655
714
|
error?: string;
|
|
656
715
|
}
|
|
657
|
-
export type CanonicalEvent = CanonicalConnected | CanonicalDisconnected | CanonicalQR | CanonicalPairSuccess | CanonicalPairError | CanonicalLoggedOut | CanonicalConnectFailure | CanonicalStreamError | CanonicalStreamReplaced | CanonicalClientOutdated | CanonicalTemporaryBan | CanonicalQrScannedWithoutMultidevice | CanonicalMessage | CanonicalReceipt | CanonicalPushNameUpdate | CanonicalContactUpdate | CanonicalPictureUpdate | CanonicalPresence | CanonicalChatPresence | CanonicalGroupUpdate | CanonicalArchiveUpdate | CanonicalPinUpdate | CanonicalMuteUpdate | CanonicalStarUpdate | CanonicalMarkChatAsReadUpdate | CanonicalLabelEdit | CanonicalLabelAssociation | CanonicalIncomingCall | CanonicalUndecryptableMessage | CanonicalLidMappingUpdate | CanonicalNewsletterLiveUpdate | CanonicalChatDelete | CanonicalChatClear | CanonicalMessageDelete | CanonicalDisappearingModeChanged | CanonicalHistorySync | CanonicalOfflineSyncCompleted | CanonicalDirtyState | CanonicalRawNode | CanonicalNotification | CanonicalMexNotification | CanonicalServerAck | CanonicalNoop;
|
|
716
|
+
export type CanonicalEvent = CanonicalConnected | CanonicalDisconnected | CanonicalQR | CanonicalPairSuccess | CanonicalPairError | CanonicalLoggedOut | CanonicalConnectFailure | CanonicalStreamError | CanonicalStreamReplaced | CanonicalClientOutdated | CanonicalTemporaryBan | CanonicalQrScannedWithoutMultidevice | CanonicalMessage | CanonicalReceipt | CanonicalPushNameUpdate | CanonicalContactUpdate | CanonicalPictureUpdate | CanonicalPresence | CanonicalChatPresence | CanonicalGroupUpdate | CanonicalArchiveUpdate | CanonicalPinUpdate | CanonicalMuteUpdate | CanonicalStarUpdate | CanonicalMarkChatAsReadUpdate | CanonicalLabelEdit | CanonicalLabelAssociation | CanonicalAppStateSyncFailed | CanonicalQrCodesExhausted | CanonicalSettingUpdate | CanonicalIncomingCall | CanonicalUndecryptableMessage | CanonicalLidMappingUpdate | CanonicalNewsletterLiveUpdate | CanonicalChatDelete | CanonicalChatClear | CanonicalMessageDelete | CanonicalDisappearingModeChanged | CanonicalHistorySync | CanonicalOfflineSyncCompleted | CanonicalDirtyState | CanonicalRawNode | CanonicalNotification | CanonicalMexNotification | CanonicalServerAck | CanonicalNoop;
|
|
658
717
|
//# sourceMappingURL=types.d.ts.map
|