@oxidezap/baileyrs 0.0.18 → 0.0.21

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.
Files changed (84) hide show
  1. package/lib/Bridge/__tests__/adapt.test.js +41 -9
  2. package/lib/Bridge/__tests__/adapt.test.js.map +1 -1
  3. package/lib/Bridge/adapt.d.ts +10 -16
  4. package/lib/Bridge/adapt.d.ts.map +1 -1
  5. package/lib/Bridge/adapt.js +12 -515
  6. package/lib/Bridge/adapt.js.map +1 -1
  7. package/lib/Bridge/schema.d.ts +36 -0
  8. package/lib/Bridge/schema.d.ts.map +1 -0
  9. package/lib/Bridge/schema.js +773 -0
  10. package/lib/Bridge/schema.js.map +1 -0
  11. package/lib/Bridge/types.d.ts +257 -3
  12. package/lib/Bridge/types.d.ts.map +1 -1
  13. package/lib/Socket/events.d.ts +18 -9
  14. package/lib/Socket/events.d.ts.map +1 -1
  15. package/lib/Socket/events.js +520 -204
  16. package/lib/Socket/events.js.map +1 -1
  17. package/lib/Socket/group-notifications.d.ts +6 -0
  18. package/lib/Socket/group-notifications.d.ts.map +1 -1
  19. package/lib/Socket/group-notifications.js +123 -16
  20. package/lib/Socket/group-notifications.js.map +1 -1
  21. package/lib/Socket/groups.d.ts +10 -2
  22. package/lib/Socket/groups.d.ts.map +1 -1
  23. package/lib/Socket/groups.js +24 -30
  24. package/lib/Socket/groups.js.map +1 -1
  25. package/lib/Socket/index.d.ts +31 -8
  26. package/lib/Socket/index.d.ts.map +1 -1
  27. package/lib/Socket/index.js +178 -13
  28. package/lib/Socket/index.js.map +1 -1
  29. package/lib/Socket/messages.d.ts +13 -2
  30. package/lib/Socket/messages.d.ts.map +1 -1
  31. package/lib/Socket/messages.js +27 -6
  32. package/lib/Socket/messages.js.map +1 -1
  33. package/lib/Types/Call.d.ts +20 -0
  34. package/lib/Types/Call.d.ts.map +1 -1
  35. package/lib/Types/Contact.d.ts +3 -1
  36. package/lib/Types/Contact.d.ts.map +1 -1
  37. package/lib/Types/Events.d.ts +2 -0
  38. package/lib/Types/Events.d.ts.map +1 -1
  39. package/lib/Utils/generics.d.ts +17 -0
  40. package/lib/Utils/generics.d.ts.map +1 -1
  41. package/lib/Utils/generics.js +31 -0
  42. package/lib/Utils/generics.js.map +1 -1
  43. package/lib/Utils/group-stub-params.d.ts +29 -0
  44. package/lib/Utils/group-stub-params.d.ts.map +1 -0
  45. package/lib/Utils/group-stub-params.js +45 -0
  46. package/lib/Utils/group-stub-params.js.map +1 -0
  47. package/lib/Utils/messages-media.d.ts +1 -1
  48. package/lib/Utils/messages-media.d.ts.map +1 -1
  49. package/lib/Utils/messages-media.js +1 -1
  50. package/lib/Utils/messages-media.js.map +1 -1
  51. package/lib/Utils/messages.d.ts.map +1 -1
  52. package/lib/Utils/messages.js +4 -6
  53. package/lib/Utils/messages.js.map +1 -1
  54. package/lib/Utils/process-history-message.d.ts +30 -0
  55. package/lib/Utils/process-history-message.d.ts.map +1 -0
  56. package/lib/Utils/process-history-message.js +148 -0
  57. package/lib/Utils/process-history-message.js.map +1 -0
  58. package/lib/Utils/use-bridge-store.d.ts.map +1 -1
  59. package/lib/Utils/use-bridge-store.js +50 -6
  60. package/lib/Utils/use-bridge-store.js.map +1 -1
  61. package/lib/Utils/use-multi-file-auth-state.d.ts +7 -0
  62. package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -1
  63. package/lib/Utils/use-multi-file-auth-state.js +11 -1
  64. package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
  65. package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
  66. package/lib/Utils/wrap-legacy-store.js +110 -90
  67. package/lib/Utils/wrap-legacy-store.js.map +1 -1
  68. package/lib/WABinary/generic-utils.d.ts +17 -0
  69. package/lib/WABinary/generic-utils.d.ts.map +1 -0
  70. package/lib/WABinary/generic-utils.js +44 -0
  71. package/lib/WABinary/generic-utils.js.map +1 -0
  72. package/lib/WABinary/index.d.ts +1 -0
  73. package/lib/WABinary/index.d.ts.map +1 -1
  74. package/lib/WABinary/index.js +1 -0
  75. package/lib/WABinary/index.js.map +1 -1
  76. package/lib/WABinary/jid-utils.d.ts +4 -0
  77. package/lib/WABinary/jid-utils.d.ts.map +1 -1
  78. package/lib/WABinary/jid-utils.js +4 -0
  79. package/lib/WABinary/jid-utils.js.map +1 -1
  80. package/lib/__tests__/expect.d.ts +14 -0
  81. package/lib/__tests__/expect.d.ts.map +1 -1
  82. package/lib/__tests__/expect.js +42 -2
  83. package/lib/__tests__/expect.js.map +1 -1
  84. package/package.json +2 -2
@@ -0,0 +1,773 @@
1
+ /**
2
+ * Schema-driven bridge → canonical adapter table.
3
+ *
4
+ * The single source of truth for "every bridge event variant we accept and
5
+ * how it maps to a CanonicalEvent". Each entry receives the bridge's typed
6
+ * data slot (`Extract<WhatsAppEvent, { type: T }>['data']`) and returns
7
+ * either a CanonicalEvent or `null` (drop on unrecoverable shape).
8
+ *
9
+ * Type safety properties:
10
+ * 1. The table is `satisfies AdapterMap` where `AdapterMap` is keyed by
11
+ * every `WhatsAppEvent['type']`. Adding a new variant to the bridge
12
+ * `.d.ts` and forgetting an entry here is a compile error.
13
+ * 2. Each entry's `data` parameter is the bridge's narrow type — fields
14
+ * come back as the right TS type without manual `unknown` checks.
15
+ * 3. Each entry's return type is `CanonicalEvent | null`. Returning the
16
+ * wrong canonical shape is a compile error.
17
+ *
18
+ * Where the bridge `.d.ts` references a sub-type that isn't separately
19
+ * exported (PinAction, MuteAction, ArchiveChatAction, etc. — the action
20
+ * field of every sync-action event), TS resolves the type to `any`. We
21
+ * apply narrowing parsers from `./primitives` (`isObject`, `asBoolOr`,
22
+ * `asNumber`, `asString`) at those leak points only — not boilerplate
23
+ * everywhere.
24
+ */
25
+ import { processHistoryMessage } from "../Utils/process-history-message.js";
26
+ import { asBoolOr, asJidString, asNumber, asString, bridgeJidToString, isBridgeJid, isObject, normalizeDiscriminator, toUnixSeconds } from "./primitives.js";
27
+ /**
28
+ * Bridge sync-action events (`pin_update`, `mute_update`, etc.) carry the
29
+ * proto action under `data.action`, but the bridge `.d.ts` types it as
30
+ * the unexported `PinAction` / `MuteAction` (resolves to `any`). Narrow
31
+ * once at the call site.
32
+ */
33
+ const extractAction = (data) => isObject(data.action) ? data.action : undefined;
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Per-event adapters — the bulk of the file. One entry per WhatsAppEvent variant.
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+ const ADAPTERS = {
38
+ // ── Connection lifecycle ──
39
+ connected: () => ({ type: 'connected' }),
40
+ disconnected: () => ({ type: 'disconnected' }),
41
+ stream_replaced: () => ({ type: 'streamReplaced' }),
42
+ client_outdated: () => ({ type: 'clientOutdated' }),
43
+ temporary_ban: data => ({
44
+ type: 'temporaryBan',
45
+ code: asNumber(data?.code),
46
+ expire: asNumber(data?.expire)
47
+ }),
48
+ qr_scanned_without_multidevice: () => ({ type: 'qrScannedWithoutMultidevice' }),
49
+ logged_out: data => ({
50
+ type: 'loggedOut',
51
+ onConnect: asBoolOr(data?.on_connect, false),
52
+ reason: asString(data?.reason)
53
+ }),
54
+ qr: data => (data.code ? { type: 'qr', code: data.code } : null),
55
+ pairing_code: data => (data.code ? { type: 'qr', code: data.code } : null),
56
+ pair_success: data => {
57
+ // `id` and `lid` come typed as `Jid` in the bridge .d.ts, but the
58
+ // bridge actually serializes pair_success.{id,lid} as strings (see
59
+ // the wire log) — accept both shapes.
60
+ const id = typeof data.id === 'string' ? data.id : asJidString(data.id);
61
+ if (!id)
62
+ return null;
63
+ return {
64
+ type: 'pairSuccess',
65
+ id,
66
+ lid: typeof data.lid === 'string' ? data.lid : asJidString(data.lid),
67
+ platform: asString(data.platform),
68
+ businessName: asString(data.business_name)
69
+ };
70
+ },
71
+ pair_error: data => ({
72
+ type: 'pairError',
73
+ error: asString(data.error) ?? 'Unknown pairing error',
74
+ id: typeof data.id === 'string' ? data.id : asJidString(data.id),
75
+ lid: typeof data.lid === 'string' ? data.lid : asJidString(data.lid),
76
+ businessName: asString(data.business_name),
77
+ platform: asString(data.platform)
78
+ }),
79
+ connect_failure: data => isObject(data)
80
+ ? { type: 'connectFailure', message: asString(data.message), reason: asNumber(data.reason) }
81
+ : { type: 'connectFailure' },
82
+ stream_error: data => ({ type: 'streamError', code: asString(data.code) ?? 'unknown' }),
83
+ // ── Messages ──
84
+ message: (data, logger) => adaptMessage(data, logger),
85
+ receipt: (data, logger) => adaptReceipt(data, logger),
86
+ undecryptable_message: data => {
87
+ // Bridge ships `{ info: MessageInfo, is_unavailable, unavailable_type,
88
+ // decrypt_fail_mode }` — same MessageInfo shape as the regular
89
+ // `message` event. Extract the chat / sender / id so the dispatcher
90
+ // can synthesize a CIPHERTEXT stub matching upstream
91
+ // `messages-recv.ts:1352`.
92
+ if (!isObject(data))
93
+ return null;
94
+ const info = isObject(data.info) ? data.info : undefined;
95
+ if (!info)
96
+ return null;
97
+ const src = isObject(info.source) ? info.source : undefined;
98
+ const chat = src && asJidString(src.chat);
99
+ const id = asString(info.id);
100
+ if (!src || !chat || !id)
101
+ return null;
102
+ const isGroup = asBoolOr(src.is_group, false);
103
+ const participantAlt = isGroup && isBridgeJid(src.sender_alt) ? bridgeJidToString(src.sender_alt) : undefined;
104
+ const remoteJidAlt = !isGroup && isBridgeJid(src.recipient_alt) ? bridgeJidToString(src.recipient_alt) : undefined;
105
+ return {
106
+ type: 'undecryptableMessage',
107
+ chatJid: chat,
108
+ senderJid: isGroup ? asJidString(src.sender) : undefined,
109
+ id,
110
+ timestamp: toUnixSeconds(info.timestamp),
111
+ isFromMe: asBoolOr(src.is_from_me, false),
112
+ isGroup,
113
+ pushName: asString(info.push_name),
114
+ participantAlt,
115
+ remoteJidAlt,
116
+ isUnavailable: asBoolOr(data.is_unavailable, false),
117
+ unavailableType: asString(data.unavailable_type),
118
+ decryptFailMode: asString(data.decrypt_fail_mode),
119
+ raw: data
120
+ };
121
+ },
122
+ // ── Contacts ──
123
+ push_name_update: data => {
124
+ const jid = asJidString(data.jid);
125
+ return jid ? { type: 'pushNameUpdate', jid, newPushName: asString(data.new_push_name) } : null;
126
+ },
127
+ contact_update: data => adaptContactUpdate(data),
128
+ contact_updated: data => adaptContactUpdate(data),
129
+ picture_update: data => {
130
+ const jid = asJidString(data.jid);
131
+ if (!jid)
132
+ return null;
133
+ return {
134
+ type: 'pictureUpdate',
135
+ jid,
136
+ removed: asBoolOr(data.removed, false),
137
+ author: asJidString(data.author),
138
+ pictureId: asString(data.picture_id)
139
+ };
140
+ },
141
+ // ── Presence ──
142
+ presence: data => {
143
+ const from = asJidString(data.from);
144
+ if (!from)
145
+ return null;
146
+ return {
147
+ type: 'presence',
148
+ from,
149
+ unavailable: asBoolOr(data.unavailable, false),
150
+ lastSeen: asNumber(data.last_seen)
151
+ };
152
+ },
153
+ chat_presence: data => {
154
+ const src = isObject(data.source) ? data.source : undefined;
155
+ if (!src)
156
+ return null;
157
+ const chat = asJidString(src.chat);
158
+ const sender = asJidString(src.sender);
159
+ if (!chat || !sender)
160
+ return null;
161
+ // Bridge sends `media: ''` for "no media" — normalize to undefined
162
+ // so consumers can rely on field omission as the absence signal.
163
+ const media = asString(data.media);
164
+ // Bridge `state` MUST be one of the two canonical values; defaulting
165
+ // to 'composing' on an unknown/missing value would synthesize a
166
+ // false typing indicator. Drop the event instead.
167
+ const rawState = asString(data.state);
168
+ if (rawState !== 'composing' && rawState !== 'paused')
169
+ return null;
170
+ return {
171
+ type: 'chatPresence',
172
+ chatJid: chat,
173
+ senderJid: sender,
174
+ state: rawState,
175
+ media: media === 'audio' ? 'audio' : undefined
176
+ };
177
+ },
178
+ // ── Groups ──
179
+ group_update: (data, logger) => adaptGroupUpdate(data, logger),
180
+ // ── Chat state ──
181
+ archive_update: data => {
182
+ const jid = asJidString(data.jid);
183
+ if (!jid)
184
+ return null;
185
+ return { type: 'archiveUpdate', jid, archived: asBoolOr(extractAction(data)?.archived, true) };
186
+ },
187
+ pin_update: data => {
188
+ const jid = asJidString(data.jid);
189
+ if (!jid)
190
+ return null;
191
+ return {
192
+ type: 'pinUpdate',
193
+ jid,
194
+ timestamp: asNumber(data.timestamp),
195
+ pinned: asBoolOr(extractAction(data)?.pinned, true)
196
+ };
197
+ },
198
+ mute_update: data => {
199
+ const jid = asJidString(data.jid);
200
+ if (!jid)
201
+ return null;
202
+ const action = extractAction(data);
203
+ return {
204
+ type: 'muteUpdate',
205
+ jid,
206
+ timestamp: asNumber(data.timestamp),
207
+ muted: asBoolOr(action?.muted, true),
208
+ muteEndTimestamp: asNumber(action?.muteEndTimestamp) ?? asNumber(action?.mute_end_timestamp)
209
+ };
210
+ },
211
+ star_update: data => adaptStarUpdate(data),
212
+ mark_chat_as_read_update: data => {
213
+ const jid = asJidString(data.jid);
214
+ if (!jid)
215
+ return null;
216
+ return { type: 'markChatAsReadUpdate', jid, read: asBoolOr(extractAction(data)?.read, true) };
217
+ },
218
+ // ── Calls ──
219
+ incoming_call: (data, logger) => adaptIncomingCall(data, logger),
220
+ // ── History sync — fully decoded by the bridge, normalized 1:1 with upstream ──
221
+ history_sync: data => {
222
+ // `data` is `proto.IHistorySync & { syncType, chunkOrder?, progress? }` —
223
+ // the bridge serialized the whole proto via `to_js_value_camel`. Walk
224
+ // it through the upstream-aligned `processHistoryMessage` to bucket
225
+ // chats / contacts / messages / lidPnMappings, then forward the
226
+ // metadata fields (the dispatcher folds in `isLatest` /
227
+ // `peerDataRequestSessionId`).
228
+ if (!isObject(data))
229
+ return null;
230
+ const processed = processHistoryMessage(data);
231
+ // Top-level metadata overlay wins over the proto's own `syncType` /
232
+ // `progress` fields when both present — the bridge maintains the
233
+ // authoritative copy across multi-chunk arrivals.
234
+ const overlay = data;
235
+ const metaSyncType = asNumber(overlay.syncType);
236
+ const metaChunkOrder = asNumber(overlay.chunkOrder);
237
+ const metaProgress = asNumber(overlay.progress);
238
+ const peerDataRequestSessionId = asString(overlay.peerDataRequestSessionId);
239
+ return {
240
+ type: 'historySync',
241
+ chats: processed.chats,
242
+ contacts: processed.contacts,
243
+ messages: processed.messages,
244
+ lidPnMappings: processed.lidPnMappings,
245
+ syncType: metaSyncType ?? processed.syncType,
246
+ progress: metaProgress ?? processed.progress,
247
+ chunkOrder: metaChunkOrder,
248
+ peerDataRequestSessionId
249
+ };
250
+ },
251
+ // ── Acknowledged but no Baileys equivalent (noop) ──
252
+ self_push_name_updated: () => ({ type: 'noop', bridgeType: 'self_push_name_updated' }),
253
+ offline_sync_completed: () => ({ type: 'noop', bridgeType: 'offline_sync_completed' }),
254
+ offline_sync_preview: () => ({ type: 'noop', bridgeType: 'offline_sync_preview' }),
255
+ device_list_update: () => ({ type: 'noop', bridgeType: 'device_list_update' }),
256
+ disappearing_mode_changed: data => {
257
+ const jid = asJidString(data.from);
258
+ const duration = asNumber(data.duration);
259
+ if (!jid || duration == null)
260
+ return { type: 'noop', bridgeType: 'disappearing_mode_changed' };
261
+ return {
262
+ type: 'disappearingModeChanged',
263
+ jid,
264
+ duration,
265
+ settingTimestamp: asNumber(data.setting_timestamp)
266
+ };
267
+ },
268
+ business_status_update: () => ({ type: 'noop', bridgeType: 'business_status_update' }),
269
+ newsletter_live_update: data => {
270
+ const newsletterJid = asJidString(data.newsletter_jid);
271
+ if (!newsletterJid)
272
+ return { type: 'noop', bridgeType: 'newsletter_live_update' };
273
+ const rawMessages = Array.isArray(data.messages) ? data.messages : [];
274
+ const messages = rawMessages
275
+ .map(m => {
276
+ if (!isObject(m))
277
+ return null;
278
+ // Prefer the original string form — `server_id` is 64-bit and
279
+ // `Number()` rounds at 2^53, so going through `asNumber` first
280
+ // would silently corrupt newsletter ids past that boundary.
281
+ const serverId = asString(m.server_id) ?? asNumber(m.server_id)?.toString();
282
+ if (!serverId)
283
+ return null;
284
+ const rawReactions = Array.isArray(m.reactions) ? m.reactions : [];
285
+ const reactions = rawReactions
286
+ .map(r => {
287
+ if (!isObject(r))
288
+ return null;
289
+ const code = asString(r.code);
290
+ const count = asNumber(r.count);
291
+ if (!code || count == null)
292
+ return null;
293
+ return { code, count };
294
+ })
295
+ .filter((r) => r !== null);
296
+ return { serverId, reactions };
297
+ })
298
+ .filter((m) => m !== null);
299
+ return { type: 'newsletterLiveUpdate', newsletterJid, messages };
300
+ },
301
+ contact_number_changed: data => {
302
+ // Bridge `ContactNumberChanged` carries up to two LID↔PN pairs:
303
+ // (old_lid, old_jid) and (new_lid, new_jid). We learn whatever's
304
+ // present and let the dispatcher fan out one upstream event per
305
+ // pair. Mirrors upstream `messages-recv.ts:287`.
306
+ const oldJid = asJidString(data.old_jid);
307
+ const newJid = asJidString(data.new_jid);
308
+ const oldLid = asJidString(data.old_lid);
309
+ const newLid = asJidString(data.new_lid);
310
+ const mappings = [];
311
+ if (oldLid && oldJid)
312
+ mappings.push({ lid: oldLid, pn: oldJid });
313
+ if (newLid && newJid)
314
+ mappings.push({ lid: newLid, pn: newJid });
315
+ if (mappings.length === 0)
316
+ return { type: 'noop', bridgeType: 'contact_number_changed' };
317
+ return { type: 'lidMappingUpdate', mappings };
318
+ },
319
+ contact_sync_requested: () => ({ type: 'noop', bridgeType: 'contact_sync_requested' }),
320
+ user_about_update: () => ({ type: 'noop', bridgeType: 'user_about_update' }),
321
+ delete_chat_update: data => {
322
+ const jid = asJidString(data.jid);
323
+ return jid ? { type: 'chatDelete', jid } : { type: 'noop', bridgeType: 'delete_chat_update' };
324
+ },
325
+ delete_message_for_me_update: data => {
326
+ const chatJid = asJidString(data.chat_jid);
327
+ const messageId = asString(data.message_id);
328
+ if (!chatJid || !messageId)
329
+ return { type: 'noop', bridgeType: 'delete_message_for_me_update' };
330
+ return {
331
+ type: 'messageDelete',
332
+ chatJid,
333
+ messageId,
334
+ fromMe: asBoolOr(data.from_me, false),
335
+ participantJid: asJidString(data.participant_jid)
336
+ };
337
+ },
338
+ // ── Generic notification / raw node ──
339
+ notification: data => {
340
+ if (!isObject(data))
341
+ return { type: 'noop', bridgeType: 'notification' };
342
+ // Validate attrs values are actually strings — bridge serde might
343
+ // emit numbers / nested objects on rare attrs (e.g. server bugs),
344
+ // and the previous unchecked cast would smuggle non-strings into
345
+ // CanonicalNotification.attrs (typed `Record<string, string>`).
346
+ // Drop non-string values, coerce the rest.
347
+ const attrs = {};
348
+ if (isObject(data.attrs)) {
349
+ for (const [k, v] of Object.entries(data.attrs)) {
350
+ if (typeof v === 'string')
351
+ attrs[k] = v;
352
+ else if (typeof v === 'number' || typeof v === 'boolean')
353
+ attrs[k] = String(v);
354
+ // Drop nested objects / null silently — they wouldn't make
355
+ // sense on a flat attrs map anyway.
356
+ }
357
+ }
358
+ return { type: 'notification', tag: asString(data.tag) ?? 'notification', attrs };
359
+ },
360
+ raw_node: data => {
361
+ // `BinaryNode` is shaped exactly like the bridge payload
362
+ // (`{ tag, attrs, content }`). Minimal sanity check on `tag`.
363
+ if (!isObject(data) || typeof data.tag !== 'string')
364
+ return null;
365
+ return { type: 'rawNode', node: data };
366
+ },
367
+ mex_notification: data => {
368
+ const opName = asString(data.op_name);
369
+ if (!opName)
370
+ return null;
371
+ const payload = isObject(data.payload) ? data.payload : {};
372
+ return {
373
+ type: 'mexNotification',
374
+ opName,
375
+ from: asJidString(data.from),
376
+ stanzaId: asString(data.stanza_id),
377
+ offline: asBoolOr(data.offline, false),
378
+ payload
379
+ };
380
+ }
381
+ };
382
+ /** Set of bridge event types we explicitly handle — derived from the table. */
383
+ export const KNOWN_BRIDGE_EVENT_TYPES = new Set(Object.keys(ADAPTERS));
384
+ /**
385
+ * Public entry point — looks up the adapter by event type and runs it.
386
+ * Mirrors the previous `adaptBridgeEvent` API exactly.
387
+ */
388
+ export const adaptBridgeEventViaSchema = (event, logger) => {
389
+ const typed = event;
390
+ const adapter = ADAPTERS[typed.type];
391
+ if (!adapter) {
392
+ logger?.debug({ eventType: typed.type }, 'unknown bridge event (no canonical mapping)');
393
+ return null;
394
+ }
395
+ // `data` cast here is the only `as` in the public path — the bridge
396
+ // runtime is the source of truth that the type matches the discriminator.
397
+ return adapter(typed.data, logger);
398
+ };
399
+ // ─────────────────────────────────────────────────────────────────────────────
400
+ // Heavy adapters — kept as named functions because the inline form would
401
+ // blow up the table. Each takes the bridge's typed data slot.
402
+ // ─────────────────────────────────────────────────────────────────────────────
403
+ const adaptMessage = (data, logger) => {
404
+ if (!isObject(data))
405
+ return null;
406
+ const info = isObject(data.info) ? data.info : undefined;
407
+ const messageProto = isObject(data.message) ? data.message : undefined;
408
+ if (!info || !messageProto)
409
+ return null;
410
+ const src = isObject(info.source) ? info.source : undefined;
411
+ const chat = src && asJidString(src.chat);
412
+ const id = asString(info.id);
413
+ if (!src || !chat || !id) {
414
+ logger?.debug({ info }, 'message adapter: missing chat/id');
415
+ return null;
416
+ }
417
+ const isGroup = asBoolOr(src.is_group, false);
418
+ const senderRaw = src.sender;
419
+ const senderJid = isGroup ? asJidString(senderRaw) : undefined;
420
+ const participantAlt = isGroup && isBridgeJid(src.sender_alt) ? bridgeJidToString(src.sender_alt) : undefined;
421
+ const remoteJidAlt = !isGroup && isBridgeJid(src.recipient_alt) ? bridgeJidToString(src.recipient_alt) : undefined;
422
+ // Bridge `EditAttribute` is one of "1"|"2"|"3"|"7"|"8" or "" (none) — narrow
423
+ // to the literal set so consumers can switch exhaustively without
424
+ // re-parsing.
425
+ const editAttributeRaw = asString(info.edit);
426
+ const editAttribute = editAttributeRaw === '1' ||
427
+ editAttributeRaw === '2' ||
428
+ editAttributeRaw === '3' ||
429
+ editAttributeRaw === '7' ||
430
+ editAttributeRaw === '8'
431
+ ? editAttributeRaw
432
+ : undefined;
433
+ return {
434
+ type: 'message',
435
+ chatJid: chat,
436
+ senderJid,
437
+ isGroup,
438
+ isFromMe: asBoolOr(src.is_from_me, false),
439
+ id,
440
+ timestamp: toUnixSeconds(info.timestamp),
441
+ pushName: asString(info.push_name),
442
+ participantAlt,
443
+ remoteJidAlt,
444
+ isViewOnce: info.is_view_once === true ? true : undefined,
445
+ isOffline: asBoolOr(info.is_offline, false) ? true : undefined,
446
+ unavailableRequestId: asString(info.unavailable_request_id),
447
+ editAttribute,
448
+ messageProto: messageProto
449
+ };
450
+ };
451
+ const adaptReceipt = (data, logger) => {
452
+ if (!isObject(data))
453
+ return null;
454
+ const src = isObject(data.source) ? data.source : undefined;
455
+ if (!src)
456
+ return null;
457
+ const chat = asJidString(src.chat);
458
+ const ids = Array.isArray(data.message_ids) ? data.message_ids.filter((x) => typeof x === 'string') : [];
459
+ if (!chat || ids.length === 0) {
460
+ logger?.debug({ data }, 'receipt adapter: missing chat or message_ids');
461
+ return null;
462
+ }
463
+ const isGroup = asBoolOr(src.is_group, false);
464
+ return {
465
+ type: 'receipt',
466
+ chatJid: chat,
467
+ senderJid: isGroup ? asJidString(src.sender) : undefined,
468
+ isGroup,
469
+ isFromMe: asBoolOr(src.is_from_me, false),
470
+ messageIds: ids,
471
+ timestamp: toUnixSeconds(data.timestamp),
472
+ receiptType: parseReceiptType(data.type)
473
+ };
474
+ };
475
+ const adaptContactUpdate = (data) => {
476
+ if (!isObject(data))
477
+ return null;
478
+ const jid = asJidString(data.jid);
479
+ if (!jid)
480
+ return null;
481
+ const action = isObject(data.action) ? data.action : undefined;
482
+ return {
483
+ type: 'contactUpdate',
484
+ jid,
485
+ fullName: asString(action?.fullName) ?? asString(action?.full_name),
486
+ firstName: asString(action?.firstName) ?? asString(action?.first_name),
487
+ lidJid: asString(action?.lidJid) ?? asString(action?.lid_jid),
488
+ pnJid: asString(action?.pnJid) ?? asString(action?.pn_jid),
489
+ username: asString(action?.username)
490
+ };
491
+ };
492
+ /**
493
+ * Bridge wire `ReceiptType` → canonical kebab-cased variant.
494
+ *
495
+ * The bridge's generated `.d.ts` advertises `ReceiptType` as
496
+ * `{type: "delivered"} | …`, but `#[serde(from = "String")]` on the rust
497
+ * enum disables the matching `Serialize` rename: the wire form is the
498
+ * bare PascalCase variant name (`"Delivered"`, `"Read"`, `"PeerMsg"`).
499
+ * Keep both spellings here so a future bridge bump that re-introduces
500
+ * the snake_case wire form keeps working.
501
+ */
502
+ const RECEIPT_TYPE_MAP = {
503
+ Delivered: 'delivered',
504
+ Sender: 'sender',
505
+ Retry: 'retry',
506
+ EncRekeyRetry: 'enc-rekey-retry',
507
+ Read: 'read',
508
+ ReadSelf: 'read-self',
509
+ Played: 'played',
510
+ PlayedSelf: 'played-self',
511
+ Inactive: 'inactive',
512
+ PeerMsg: 'peer-msg',
513
+ HistorySync: 'history-sync',
514
+ ServerError: 'server-error',
515
+ delivered: 'delivered',
516
+ sender: 'sender',
517
+ retry: 'retry',
518
+ enc_rekey_retry: 'enc-rekey-retry',
519
+ read: 'read',
520
+ read_self: 'read-self',
521
+ played: 'played',
522
+ played_self: 'played-self',
523
+ inactive: 'inactive',
524
+ peer_msg: 'peer-msg',
525
+ history_sync: 'history-sync',
526
+ server_error: 'server-error'
527
+ };
528
+ const parseReceiptType = (raw) => {
529
+ const norm = typeof raw === 'string' ? raw : isObject(raw) && typeof raw.type === 'string' ? raw.type : undefined;
530
+ if (norm == null)
531
+ return undefined;
532
+ return RECEIPT_TYPE_MAP[norm] ?? 'other';
533
+ };
534
+ const adaptStarUpdate = (data) => {
535
+ if (!isObject(data))
536
+ return null;
537
+ const chatJid = asJidString(data.chat_jid);
538
+ const messageId = asString(data.message_id);
539
+ if (!chatJid || !messageId)
540
+ return null;
541
+ const action = isObject(data.action) ? data.action : undefined;
542
+ return {
543
+ type: 'starUpdate',
544
+ chatJid,
545
+ messageId,
546
+ fromMe: asBoolOr(data.from_me, false),
547
+ participantJid: asJidString(data.participant_jid),
548
+ starred: asBoolOr(action?.starred, false)
549
+ };
550
+ };
551
+ const adaptIncomingCall = (data, logger) => {
552
+ if (!isObject(data))
553
+ return null;
554
+ const from = asJidString(data.from);
555
+ if (!from)
556
+ return null;
557
+ const action = isObject(data.action) ? data.action : undefined;
558
+ if (!action)
559
+ return null;
560
+ const actionType = parseCallActionType(action.type);
561
+ const callId = asString(action.call_id);
562
+ if (!actionType || !callId) {
563
+ logger?.debug({ data }, 'incoming_call adapter: missing action.type/call_id');
564
+ return null;
565
+ }
566
+ const canonicalAction = { type: actionType, callId };
567
+ // The bridge `CallAction` union narrows variant-specific fields only
568
+ // on the matching variant. We've already validated `action.type` via
569
+ // `parseCallActionType`, so a runtime cast through the action's own
570
+ // shape is safe — reading fields off `unknown` is the cheapest way
571
+ // around TS's inability to follow our string-normalized discriminator
572
+ // back into the bridge union.
573
+ const fields = action;
574
+ if (actionType === 'offer') {
575
+ canonicalAction.callerPn = asJidString(fields.caller_pn);
576
+ canonicalAction.callerCountryCode = asString(fields.caller_country_code);
577
+ canonicalAction.deviceClass = asString(fields.device_class);
578
+ canonicalAction.joinable = asBoolOr(fields.joinable, false);
579
+ canonicalAction.isVideo = asBoolOr(fields.is_video, false);
580
+ if (Array.isArray(fields.audio)) {
581
+ canonicalAction.audio = fields.audio.filter((x) => typeof x === 'string');
582
+ }
583
+ }
584
+ else if (actionType === 'terminate') {
585
+ canonicalAction.duration = asNumber(fields.duration);
586
+ canonicalAction.audioDuration = asNumber(fields.audio_duration);
587
+ }
588
+ return {
589
+ type: 'incomingCall',
590
+ from,
591
+ timestamp: toUnixSeconds(data.timestamp),
592
+ offline: asBoolOr(data.offline, false),
593
+ stanzaId: asString(data.stanza_id),
594
+ notify: asString(data.notify),
595
+ platform: asString(data.platform),
596
+ version: asString(data.version),
597
+ action: canonicalAction
598
+ };
599
+ };
600
+ const parseCallActionType = (raw) => {
601
+ const norm = normalizeDiscriminator(raw);
602
+ switch (norm) {
603
+ case 'offer':
604
+ return 'offer';
605
+ case 'pre_accept':
606
+ case 'preaccept':
607
+ return 'preAccept';
608
+ case 'accept':
609
+ return 'accept';
610
+ case 'reject':
611
+ return 'reject';
612
+ case 'terminate':
613
+ return 'terminate';
614
+ default:
615
+ return undefined;
616
+ }
617
+ };
618
+ // ── Group action adapter ──
619
+ const adaptGroupParticipant = (raw) => {
620
+ if (!isObject(raw))
621
+ return null;
622
+ const jid = asJidString(raw.jid);
623
+ if (!jid)
624
+ return null;
625
+ return { jid, phoneNumber: asJidString(raw.phone_number) };
626
+ };
627
+ const adaptGroupParticipants = (raw) => {
628
+ if (!Array.isArray(raw))
629
+ return [];
630
+ return raw.map(adaptGroupParticipant).filter((p) => p !== null);
631
+ };
632
+ const adaptGroupAction = (raw) => {
633
+ if (!isObject(raw))
634
+ return null;
635
+ const norm = normalizeDiscriminator(raw.type);
636
+ const rawType = asString(raw.type) ?? 'unknown';
637
+ if (!norm)
638
+ return { type: 'unknown', rawType };
639
+ switch (norm) {
640
+ case 'add':
641
+ return { type: 'add', participants: adaptGroupParticipants(raw.participants), reason: asString(raw.reason) };
642
+ case 'remove':
643
+ return { type: 'remove', participants: adaptGroupParticipants(raw.participants), reason: asString(raw.reason) };
644
+ case 'promote':
645
+ return { type: 'promote', participants: adaptGroupParticipants(raw.participants) };
646
+ case 'demote':
647
+ return { type: 'demote', participants: adaptGroupParticipants(raw.participants) };
648
+ case 'modify':
649
+ return { type: 'modify', participants: adaptGroupParticipants(raw.participants) };
650
+ case 'subject':
651
+ return {
652
+ type: 'subject',
653
+ subject: asString(raw.subject) ?? '',
654
+ subjectOwner: asJidString(raw.subject_owner),
655
+ subjectTime: asNumber(raw.subject_time)
656
+ };
657
+ case 'description':
658
+ return {
659
+ type: 'description',
660
+ id: asString(raw.id) ?? '',
661
+ description: asString(raw.description)
662
+ };
663
+ case 'locked':
664
+ return { type: 'locked' };
665
+ case 'unlocked':
666
+ return { type: 'unlocked' };
667
+ case 'announce':
668
+ case 'announcement':
669
+ return { type: 'announce' };
670
+ case 'not_announce':
671
+ case 'not_announcement':
672
+ case 'notannounce':
673
+ return { type: 'notAnnounce' };
674
+ case 'ephemeral': {
675
+ const expiration = asNumber(raw.expiration);
676
+ if (expiration == null)
677
+ return null;
678
+ return { type: 'ephemeral', expiration, trigger: asNumber(raw.trigger) };
679
+ }
680
+ case 'membership_approval_mode':
681
+ case 'membershipapprovalmode':
682
+ return { type: 'membershipApprovalMode', enabled: asBoolOr(raw.enabled, false) };
683
+ case 'member_add_mode':
684
+ case 'memberaddmode':
685
+ return { type: 'memberAddMode', mode: asString(raw.mode) ?? '' };
686
+ case 'no_frequently_forwarded':
687
+ case 'nofrequentlyforwarded':
688
+ return { type: 'noFrequentlyForwarded' };
689
+ case 'frequently_forwarded_ok':
690
+ case 'frequentlyforwardedok':
691
+ return { type: 'frequentlyForwardedOk' };
692
+ case 'invite':
693
+ return { type: 'invite', code: asString(raw.code) ?? '' };
694
+ case 'revoke':
695
+ case 'revoke_invite':
696
+ case 'revokeinvite':
697
+ return { type: 'revokeInvite' };
698
+ case 'create':
699
+ return { type: 'create' };
700
+ case 'delete':
701
+ return { type: 'delete', reason: asString(raw.reason) };
702
+ case 'link':
703
+ return { type: 'link', linkType: asString(raw.link_type) ?? '' };
704
+ case 'unlink':
705
+ return {
706
+ type: 'unlink',
707
+ unlinkType: asString(raw.unlink_type) ?? '',
708
+ unlinkReason: asString(raw.unlink_reason)
709
+ };
710
+ case 'growth_locked':
711
+ case 'growthlocked': {
712
+ const expiration = asNumber(raw.expiration);
713
+ if (expiration == null)
714
+ return null;
715
+ return { type: 'growthLocked', expiration, lockType: asString(raw.lock_type) ?? '' };
716
+ }
717
+ case 'growth_unlocked':
718
+ case 'growthunlocked':
719
+ return { type: 'growthUnlocked' };
720
+ case 'membership_approval_request':
721
+ return {
722
+ type: 'membershipApprovalRequest',
723
+ requestMethod: asString(raw.request_method),
724
+ parentGroupJid: asJidString(raw.parent_group_jid)
725
+ };
726
+ case 'created_membership_requests':
727
+ return {
728
+ type: 'createdMembershipRequests',
729
+ requestMethod: asString(raw.request_method),
730
+ parentGroupJid: asJidString(raw.parent_group_jid),
731
+ requests: adaptGroupParticipants(raw.requests)
732
+ };
733
+ case 'revoked_membership_requests':
734
+ return {
735
+ type: 'revokedMembershipRequests',
736
+ // `revoked_membership_requests` ships bare Jids (not full
737
+ // participant infos), so wrap each into the canonical
738
+ // participant shape with no `phoneNumber`.
739
+ participants: Array.isArray(raw.participants)
740
+ ? raw.participants
741
+ .map(j => {
742
+ const jid = asJidString(j);
743
+ return jid ? { jid } : null;
744
+ })
745
+ .filter((p) => p !== null)
746
+ : []
747
+ };
748
+ default:
749
+ return { type: 'unknown', rawType };
750
+ }
751
+ };
752
+ const adaptGroupUpdate = (data, logger) => {
753
+ if (!isObject(data))
754
+ return null;
755
+ const groupJid = asJidString(data.group_jid);
756
+ if (!groupJid)
757
+ return null;
758
+ const action = adaptGroupAction(data.action);
759
+ if (!action) {
760
+ logger?.warn({ data }, 'group_update adapter: action shape rejected');
761
+ return null;
762
+ }
763
+ return {
764
+ type: 'groupUpdate',
765
+ groupJid,
766
+ author: asJidString(data.participant),
767
+ authorPn: asJidString(data.participant_pn),
768
+ timestamp: toUnixSeconds(data.timestamp),
769
+ isLidAddressingMode: asBoolOr(data.is_lid_addressing_mode, false),
770
+ action
771
+ };
772
+ };
773
+ //# sourceMappingURL=schema.js.map