@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.
- package/lib/Bridge/__tests__/adapt.test.js +41 -9
- package/lib/Bridge/__tests__/adapt.test.js.map +1 -1
- package/lib/Bridge/adapt.d.ts +10 -16
- package/lib/Bridge/adapt.d.ts.map +1 -1
- package/lib/Bridge/adapt.js +12 -515
- package/lib/Bridge/adapt.js.map +1 -1
- package/lib/Bridge/schema.d.ts +36 -0
- package/lib/Bridge/schema.d.ts.map +1 -0
- package/lib/Bridge/schema.js +773 -0
- package/lib/Bridge/schema.js.map +1 -0
- package/lib/Bridge/types.d.ts +257 -3
- package/lib/Bridge/types.d.ts.map +1 -1
- package/lib/Socket/events.d.ts +18 -9
- package/lib/Socket/events.d.ts.map +1 -1
- package/lib/Socket/events.js +520 -204
- package/lib/Socket/events.js.map +1 -1
- package/lib/Socket/group-notifications.d.ts +6 -0
- package/lib/Socket/group-notifications.d.ts.map +1 -1
- package/lib/Socket/group-notifications.js +123 -16
- package/lib/Socket/group-notifications.js.map +1 -1
- package/lib/Socket/groups.d.ts +10 -2
- package/lib/Socket/groups.d.ts.map +1 -1
- package/lib/Socket/groups.js +24 -30
- package/lib/Socket/groups.js.map +1 -1
- package/lib/Socket/index.d.ts +31 -8
- package/lib/Socket/index.d.ts.map +1 -1
- package/lib/Socket/index.js +178 -13
- package/lib/Socket/index.js.map +1 -1
- package/lib/Socket/messages.d.ts +13 -2
- package/lib/Socket/messages.d.ts.map +1 -1
- package/lib/Socket/messages.js +27 -6
- package/lib/Socket/messages.js.map +1 -1
- package/lib/Types/Call.d.ts +20 -0
- package/lib/Types/Call.d.ts.map +1 -1
- package/lib/Types/Contact.d.ts +3 -1
- package/lib/Types/Contact.d.ts.map +1 -1
- package/lib/Types/Events.d.ts +2 -0
- package/lib/Types/Events.d.ts.map +1 -1
- package/lib/Utils/generics.d.ts +17 -0
- package/lib/Utils/generics.d.ts.map +1 -1
- package/lib/Utils/generics.js +31 -0
- package/lib/Utils/generics.js.map +1 -1
- package/lib/Utils/group-stub-params.d.ts +29 -0
- package/lib/Utils/group-stub-params.d.ts.map +1 -0
- package/lib/Utils/group-stub-params.js +45 -0
- package/lib/Utils/group-stub-params.js.map +1 -0
- package/lib/Utils/messages-media.d.ts +1 -1
- package/lib/Utils/messages-media.d.ts.map +1 -1
- package/lib/Utils/messages-media.js +1 -1
- package/lib/Utils/messages-media.js.map +1 -1
- package/lib/Utils/messages.d.ts.map +1 -1
- package/lib/Utils/messages.js +4 -6
- package/lib/Utils/messages.js.map +1 -1
- package/lib/Utils/process-history-message.d.ts +30 -0
- package/lib/Utils/process-history-message.d.ts.map +1 -0
- package/lib/Utils/process-history-message.js +148 -0
- package/lib/Utils/process-history-message.js.map +1 -0
- package/lib/Utils/use-bridge-store.d.ts.map +1 -1
- package/lib/Utils/use-bridge-store.js +50 -6
- package/lib/Utils/use-bridge-store.js.map +1 -1
- package/lib/Utils/use-multi-file-auth-state.d.ts +7 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -1
- package/lib/Utils/use-multi-file-auth-state.js +11 -1
- package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
- package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
- package/lib/Utils/wrap-legacy-store.js +110 -90
- package/lib/Utils/wrap-legacy-store.js.map +1 -1
- package/lib/WABinary/generic-utils.d.ts +17 -0
- package/lib/WABinary/generic-utils.d.ts.map +1 -0
- package/lib/WABinary/generic-utils.js +44 -0
- package/lib/WABinary/generic-utils.js.map +1 -0
- package/lib/WABinary/index.d.ts +1 -0
- package/lib/WABinary/index.d.ts.map +1 -1
- package/lib/WABinary/index.js +1 -0
- package/lib/WABinary/index.js.map +1 -1
- package/lib/WABinary/jid-utils.d.ts +4 -0
- package/lib/WABinary/jid-utils.d.ts.map +1 -1
- package/lib/WABinary/jid-utils.js +4 -0
- package/lib/WABinary/jid-utils.js.map +1 -1
- package/lib/__tests__/expect.d.ts +14 -0
- package/lib/__tests__/expect.d.ts.map +1 -1
- package/lib/__tests__/expect.js +42 -2
- package/lib/__tests__/expect.js.map +1 -1
- package/package.json +2 -2
package/lib/Socket/events.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical → Baileys event dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* `adaptBridgeEvent` (Bridge/schema.ts) yields a `CanonicalEvent`
|
|
5
|
+
* discriminated union; `DISPATCHERS` is a `{ [K in CanonicalEvent['type']]: … }`
|
|
6
|
+
* mapped type so TS forces a handler per variant — a missing handler
|
|
7
|
+
* is a compile error.
|
|
8
|
+
*/
|
|
1
9
|
import { adaptBridgeEvent } from "../Bridge/index.js";
|
|
2
10
|
import { DisconnectReason, WAProto } from "../Types/index.js";
|
|
3
11
|
import { Boom } from "../Utils/boom.js";
|
|
4
12
|
import { isJidGroup } from "../WABinary/jid-utils.js";
|
|
5
|
-
import { buildGroupNotificationDomainEvent, buildGroupNotificationStubMessages } from "./group-notifications.js";
|
|
13
|
+
import { buildGroupJoinRequestEvents, buildGroupNotificationDomainEvent, buildGroupNotificationStubMessages } from "./group-notifications.js";
|
|
6
14
|
import { mapReachoutTimelock } from "./reachout.js";
|
|
7
15
|
const DEF_CALLBACK_PREFIX = 'CB:';
|
|
8
16
|
const DEF_TAG_PREFIX = 'TAG:';
|
|
@@ -19,6 +27,11 @@ const emitCBEvents = (ctx, node) => {
|
|
|
19
27
|
if (l2)
|
|
20
28
|
ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val},${l2}`, node);
|
|
21
29
|
ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${val}`, node);
|
|
30
|
+
// Bare-key pattern (no value): upstream `socket.ts:610` emits this
|
|
31
|
+
// so consumers wiring `ws.on('CB:iq,id', …)` (any id) catch every
|
|
32
|
+
// stanza carrying that attr. Missing here meant id-pattern matchers
|
|
33
|
+
// silently never fired.
|
|
34
|
+
ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, node);
|
|
22
35
|
}
|
|
23
36
|
if (l2)
|
|
24
37
|
ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, node);
|
|
@@ -50,232 +63,535 @@ const canonicalMessageToWAMessage = (m) => {
|
|
|
50
63
|
wm.key.remoteJidAlt = m.remoteJidAlt;
|
|
51
64
|
if (m.isViewOnce)
|
|
52
65
|
wm.key.isViewOnce = true;
|
|
66
|
+
// Surface the raw EditAttribute so consumers that dedupe by `key.id`
|
|
67
|
+
// can branch on edits ("1") vs originals (""). Mirrors upstream's
|
|
68
|
+
// `decodeMessageNode` which sets `key.editAttribute` from the same
|
|
69
|
+
// stanza attribute.
|
|
70
|
+
if (m.editAttribute)
|
|
71
|
+
wm.key.editAttribute = m.editAttribute;
|
|
53
72
|
return wm;
|
|
54
73
|
};
|
|
74
|
+
const emitClose = (ctx, reason, statusCode) => ctx.ev.emit('connection.update', {
|
|
75
|
+
connection: 'close',
|
|
76
|
+
lastDisconnect: { error: new Boom(reason, { statusCode }), date: new Date() }
|
|
77
|
+
});
|
|
55
78
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* leaks into the canonical contract.
|
|
79
|
+
* Map bridge `ConnectFailureReason` wire codes (per the bridge's
|
|
80
|
+
* `.d.ts` annotation) onto upstream Baileys' `DisconnectReason`.
|
|
81
|
+
* Unknown codes fall through to `connectionClosed` so existing
|
|
82
|
+
* reconnect heuristics keep working.
|
|
61
83
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
const mapConnectFailureToDisconnect = (reason) => {
|
|
85
|
+
switch (reason) {
|
|
86
|
+
case 401: // LoggedOut
|
|
87
|
+
case 403: // MainDeviceGone
|
|
88
|
+
case 406: // UnknownLogout
|
|
89
|
+
return DisconnectReason.loggedOut;
|
|
90
|
+
case 402: // TempBanned
|
|
91
|
+
return DisconnectReason.forbidden;
|
|
92
|
+
case 405: // ClientOutdated
|
|
93
|
+
return DisconnectReason.badSession;
|
|
94
|
+
case 411: // MultideviceMismatch (legacy alias)
|
|
95
|
+
return DisconnectReason.multideviceMismatch;
|
|
96
|
+
case 503: // ServiceUnavailable
|
|
97
|
+
case 501: // Experimental
|
|
98
|
+
return DisconnectReason.unavailableService;
|
|
99
|
+
case 408: // Timed out
|
|
100
|
+
return DisconnectReason.timedOut;
|
|
101
|
+
case 515: // RestartRequired
|
|
102
|
+
return DisconnectReason.restartRequired;
|
|
103
|
+
// 400, 409, 413, 414, 415, 418, 500, undefined → generic close
|
|
104
|
+
default:
|
|
105
|
+
return DisconnectReason.connectionClosed;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const describeTempBan = (code) => {
|
|
109
|
+
switch (code) {
|
|
110
|
+
case 101:
|
|
111
|
+
return 'sent_to_too_many_people';
|
|
112
|
+
case 102:
|
|
113
|
+
return 'blocked_by_users';
|
|
114
|
+
case 103:
|
|
115
|
+
return 'created_too_many_groups';
|
|
116
|
+
case 104:
|
|
117
|
+
return 'sent_too_many_same_message';
|
|
118
|
+
case 106:
|
|
119
|
+
return 'broadcast_list';
|
|
120
|
+
default:
|
|
121
|
+
return code != null ? `code_${code}` : 'unknown';
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Sole `as` cast in the dispatch path. The bridge runtime's
|
|
126
|
+
* `connection.update` slot accepts the full ConnectionState union, but we
|
|
127
|
+
* only ever emit partials shaped as `{ connection, … }`. The Baileys event
|
|
128
|
+
* map types `connection.update` as the full state which is unsafe to
|
|
129
|
+
* widen; centralizing the cast here keeps every dispatcher clean.
|
|
130
|
+
*/
|
|
131
|
+
const emitConnectionUpdate = (ctx, update) => ctx.ev.emit('connection.update', update);
|
|
132
|
+
const DISPATCHERS = {
|
|
133
|
+
// ── Connection lifecycle ──
|
|
134
|
+
connected: (_, { ctx }) => emitConnectionUpdate(ctx, { connection: 'open' }),
|
|
135
|
+
disconnected: (_, { ctx }) => emitClose(ctx, 'Connection closed', DisconnectReason.connectionClosed),
|
|
136
|
+
qr: (evt, { ctx }) => emitConnectionUpdate(ctx, { qr: evt.code }),
|
|
137
|
+
pairSuccess: (evt, { ctx, callbacks }) => {
|
|
138
|
+
const { id, lid, businessName, platform } = evt;
|
|
139
|
+
ctx.setUser({ id, lid });
|
|
140
|
+
callbacks?.onPairSuccess?.({ platform, businessName });
|
|
141
|
+
// Synthetic `creds.update` so upstream-style auth code
|
|
142
|
+
// (`ev.on('creds.update', saveCreds)`) gets a single tick to persist
|
|
143
|
+
// post-pair state. The bridge owns the real creds; this emission is
|
|
144
|
+
// a compat hook for upstream's lifecycle.
|
|
145
|
+
ctx.ev.emit('creds.update', { registered: true, me: { id, lid, name: businessName }, platform });
|
|
146
|
+
},
|
|
147
|
+
pairError: (evt, { ctx }) => emitClose(ctx, 'Pairing failed: ' + evt.error, DisconnectReason.connectionClosed),
|
|
148
|
+
loggedOut: (evt, { ctx }) => emitClose(ctx, evt.reason ? `Logged out: ${evt.reason}` : 'Logged out', DisconnectReason.loggedOut),
|
|
149
|
+
connectFailure: (evt, { ctx }) => {
|
|
150
|
+
// Map bridge `ConnectFailureReason` wire codes onto Baileys'
|
|
151
|
+
// DisconnectReason. Defaults to connectionClosed for unknown codes.
|
|
152
|
+
// LoggedOut paths (401/403/406) drive bots' "should I re-pair?"
|
|
153
|
+
// branch — folding them into connectionClosed kept that broken.
|
|
154
|
+
const status = mapConnectFailureToDisconnect(evt.reason);
|
|
155
|
+
emitClose(ctx, evt.message ?? 'Connection failure', status);
|
|
156
|
+
},
|
|
157
|
+
streamError: (evt, { ctx }) => emitClose(ctx, 'Stream error: ' + evt.code, DisconnectReason.badSession),
|
|
158
|
+
streamReplaced: (_, { ctx }) => emitConnectionUpdate(ctx, {
|
|
65
159
|
connection: 'close',
|
|
66
|
-
lastDisconnect: {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// its post-pair state. The bridge owns the real creds; this
|
|
88
|
-
// emission is a compat hook for upstream Baileys' lifecycle.
|
|
89
|
-
ev.emit('creds.update', {
|
|
90
|
-
registered: true,
|
|
91
|
-
me: { id, lid, name: businessName },
|
|
92
|
-
platform
|
|
93
|
-
});
|
|
94
|
-
return;
|
|
160
|
+
lastDisconnect: {
|
|
161
|
+
error: new Boom('Connection replaced', { statusCode: DisconnectReason.connectionReplaced }),
|
|
162
|
+
date: new Date()
|
|
163
|
+
}
|
|
164
|
+
}),
|
|
165
|
+
clientOutdated: (_, { ctx }) => emitClose(ctx, 'Client outdated', DisconnectReason.badSession),
|
|
166
|
+
temporaryBan: (evt, { ctx }) => {
|
|
167
|
+
// Surface the wire code + expire on the Boom so consumers reading
|
|
168
|
+
// `lastDisconnect.error.data` can act on the specific reason.
|
|
169
|
+
const reason = describeTempBan(evt.code);
|
|
170
|
+
const message = evt.expire
|
|
171
|
+
? `Temporary ban (${reason}); expires at ${new Date(evt.expire * 1000).toISOString()}`
|
|
172
|
+
: `Temporary ban (${reason})`;
|
|
173
|
+
ctx.ev.emit('connection.update', {
|
|
174
|
+
connection: 'close',
|
|
175
|
+
lastDisconnect: {
|
|
176
|
+
error: new Boom(message, {
|
|
177
|
+
statusCode: DisconnectReason.forbidden,
|
|
178
|
+
data: { code: evt.code, expire: evt.expire }
|
|
179
|
+
}),
|
|
180
|
+
date: new Date()
|
|
95
181
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
qrScannedWithoutMultidevice: (_, { ctx }) => ctx.logger.warn('QR scanned but multi-device not enabled on phone'),
|
|
185
|
+
// ── Messages ──
|
|
186
|
+
message: (evt, { ctx }) => {
|
|
187
|
+
if (ctx.fullConfig.shouldIgnoreJid?.(evt.chatJid))
|
|
188
|
+
return;
|
|
189
|
+
// Note: `emitOwnEvents=false` is NOT applied here. Upstream Baileys
|
|
190
|
+
// uses that flag to suppress the local echo when `sendMessage()`
|
|
191
|
+
// succeeds, not to drop inbound `fromMe` messages from other linked
|
|
192
|
+
// devices.
|
|
193
|
+
const waMsg = canonicalMessageToWAMessage(evt);
|
|
194
|
+
const upsertPayload = {
|
|
195
|
+
messages: [waMsg],
|
|
196
|
+
type: evt.isOffline ? 'append' : 'notify'
|
|
197
|
+
};
|
|
198
|
+
if (evt.unavailableRequestId)
|
|
199
|
+
upsertPayload.requestId = evt.unavailableRequestId;
|
|
200
|
+
ctx.ev.emit('messages.upsert', upsertPayload);
|
|
201
|
+
// Mirror upstream `process-message.ts:523-533`: when the inbound
|
|
202
|
+
// proto carries a `reactionMessage`, surface it on the dedicated
|
|
203
|
+
// `messages.reaction` channel as well. The `key` in the event
|
|
204
|
+
// payload is the TARGET (the message being reacted to), and
|
|
205
|
+
// `reaction.key` is the reaction's own envelope key — that's the
|
|
206
|
+
// exact shape upstream consumers index by.
|
|
207
|
+
const reactionMessage = evt.messageProto.reactionMessage;
|
|
208
|
+
if (reactionMessage?.key) {
|
|
209
|
+
ctx.ev.emit('messages.reaction', [
|
|
210
|
+
{
|
|
211
|
+
key: reactionMessage.key,
|
|
212
|
+
reaction: { ...reactionMessage, key: waMsg.key }
|
|
213
|
+
}
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
// Mirror upstream `process-message.ts:404-414` (REVOKE) and
|
|
217
|
+
// `:474-490` (MESSAGE_EDIT). Both surface as `messages.update` with
|
|
218
|
+
// the TARGET key (id taken from protocolMessage.key).
|
|
219
|
+
const protocolMsg = evt.messageProto.protocolMessage;
|
|
220
|
+
// `group.member-tag.update` doesn't reuse `protocolMsg.key.id` — the
|
|
221
|
+
// label change applies to the participant directly, identified by
|
|
222
|
+
// the envelope key. Mirror upstream `process-message.ts:492-503`.
|
|
223
|
+
if (protocolMsg?.type === WAProto.Message.ProtocolMessage.Type.GROUP_MEMBER_LABEL_CHANGE &&
|
|
224
|
+
protocolMsg.memberLabel?.label) {
|
|
225
|
+
// `senderJid` is required for this event — it identifies the
|
|
226
|
+
// participant whose label changed. If the bridge ever surfaced a
|
|
227
|
+
// label-change without one (server bug / non-group context), an
|
|
228
|
+
// empty-string fallback would corrupt downstream consumer state.
|
|
229
|
+
// Skip with a warn instead.
|
|
230
|
+
if (!evt.senderJid) {
|
|
231
|
+
ctx.logger.warn({ chatJid: evt.chatJid, label: protocolMsg.memberLabel.label }, 'GROUP_MEMBER_LABEL_CHANGE without senderJid — dropping (no participant to attribute)');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
ctx.ev.emit('group.member-tag.update', {
|
|
235
|
+
groupId: evt.chatJid,
|
|
236
|
+
label: protocolMsg.memberLabel.label,
|
|
237
|
+
participant: evt.senderJid,
|
|
238
|
+
participantAlt: evt.participantAlt,
|
|
239
|
+
messageTimestamp: evt.timestamp
|
|
115
240
|
});
|
|
116
|
-
return;
|
|
117
|
-
case 'clientOutdated':
|
|
118
|
-
emitClose('Client outdated', DisconnectReason.badSession);
|
|
119
|
-
return;
|
|
120
|
-
case 'temporaryBan':
|
|
121
|
-
emitClose('Temporary ban', DisconnectReason.forbidden);
|
|
122
|
-
return;
|
|
123
|
-
case 'qrScannedWithoutMultidevice':
|
|
124
|
-
ctx.logger.warn('QR scanned but multi-device not enabled on phone');
|
|
125
|
-
return;
|
|
126
|
-
// ── Messages ──
|
|
127
|
-
case 'message': {
|
|
128
|
-
if (ctx.fullConfig.shouldIgnoreJid?.(evt.chatJid))
|
|
129
|
-
return;
|
|
130
|
-
// Note: `emitOwnEvents=false` is NOT applied here. Upstream
|
|
131
|
-
// Baileys uses that flag to suppress the local echo when
|
|
132
|
-
// `sendMessage()` succeeds, not to drop inbound `fromMe`
|
|
133
|
-
// messages from other linked devices.
|
|
134
|
-
const waMsg = canonicalMessageToWAMessage(evt);
|
|
135
|
-
ev.emit('messages.upsert', { messages: [waMsg], type: 'notify' });
|
|
136
|
-
return;
|
|
137
241
|
}
|
|
138
|
-
|
|
139
|
-
|
|
242
|
+
}
|
|
243
|
+
const protocolKeyId = protocolMsg?.key?.id;
|
|
244
|
+
if (protocolMsg && protocolKeyId) {
|
|
245
|
+
if (protocolMsg.type === WAProto.Message.ProtocolMessage.Type.REVOKE) {
|
|
246
|
+
ctx.ev.emit('messages.update', [
|
|
140
247
|
{
|
|
141
|
-
key: {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
},
|
|
147
|
-
receipt: { receiptTimestamp: evt.timestamp }
|
|
148
|
-
}
|
|
149
|
-
]);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
case 'undecryptableMessage':
|
|
153
|
-
ctx.logger.warn({ event: evt.raw }, 'undecryptable message received');
|
|
154
|
-
return;
|
|
155
|
-
// ── Contacts ──
|
|
156
|
-
case 'pushNameUpdate':
|
|
157
|
-
ev.emit('contacts.update', [{ id: evt.jid, notify: evt.newPushName }]);
|
|
158
|
-
return;
|
|
159
|
-
case 'contactUpdate':
|
|
160
|
-
ev.emit('contacts.update', [{ id: evt.jid }]);
|
|
161
|
-
return;
|
|
162
|
-
case 'pictureUpdate':
|
|
163
|
-
ev.emit('contacts.update', [{ id: evt.jid, imgUrl: 'changed' }]);
|
|
164
|
-
return;
|
|
165
|
-
// ── Presence ──
|
|
166
|
-
case 'presence':
|
|
167
|
-
ev.emit('presence.update', {
|
|
168
|
-
id: evt.from,
|
|
169
|
-
presences: {
|
|
170
|
-
[evt.from]: {
|
|
171
|
-
lastKnownPresence: (evt.unavailable ? 'unavailable' : 'available'),
|
|
172
|
-
lastSeen: evt.lastSeen
|
|
248
|
+
key: { ...waMsg.key, id: protocolKeyId },
|
|
249
|
+
update: {
|
|
250
|
+
message: null,
|
|
251
|
+
messageStubType: WAProto.WebMessageInfo.StubType.REVOKE,
|
|
252
|
+
key: waMsg.key
|
|
173
253
|
}
|
|
174
254
|
}
|
|
175
|
-
|
|
176
|
-
return;
|
|
177
|
-
case 'chatPresence':
|
|
178
|
-
ev.emit('presence.update', {
|
|
179
|
-
id: evt.chatJid,
|
|
180
|
-
presences: {
|
|
181
|
-
[evt.senderJid]: { lastKnownPresence: evt.state }
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
return;
|
|
185
|
-
// ── Groups ──
|
|
186
|
-
case 'groupUpdate': {
|
|
187
|
-
const user = ctx.getUser();
|
|
188
|
-
const fromMe = !!(evt.author && (evt.author === user?.id || evt.author === user?.lid));
|
|
189
|
-
const domainEvent = buildGroupNotificationDomainEvent(evt);
|
|
190
|
-
if (domainEvent)
|
|
191
|
-
ev.emit(domainEvent.name, domainEvent.payload);
|
|
192
|
-
const stubMessages = buildGroupNotificationStubMessages(evt, fromMe);
|
|
193
|
-
if (stubMessages.length > 0) {
|
|
194
|
-
ev.emit('messages.upsert', { messages: stubMessages, type: 'notify' });
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
255
|
+
]);
|
|
197
256
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
case 'pinUpdate':
|
|
203
|
-
ev.emit('chats.update', [{ id: evt.jid, pinned: evt.timestamp }]);
|
|
204
|
-
return;
|
|
205
|
-
case 'muteUpdate':
|
|
206
|
-
ev.emit('chats.update', [{ id: evt.jid, muteEndTime: evt.timestamp }]);
|
|
207
|
-
return;
|
|
208
|
-
case 'starUpdate':
|
|
209
|
-
ev.emit('messages.update', [
|
|
257
|
+
else if (protocolMsg.type === WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT && protocolMsg.editedMessage) {
|
|
258
|
+
const tsMs = protocolMsg.timestampMs;
|
|
259
|
+
const editedTs = tsMs != null ? Math.floor(Number(tsMs) / 1000) : (evt.timestamp ?? waMsg.messageTimestamp);
|
|
260
|
+
ctx.ev.emit('messages.update', [
|
|
210
261
|
{
|
|
211
|
-
key: {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
},
|
|
217
|
-
update: { starred: evt.starred }
|
|
262
|
+
key: { ...waMsg.key, id: protocolKeyId },
|
|
263
|
+
update: {
|
|
264
|
+
message: { editedMessage: { message: protocolMsg.editedMessage } },
|
|
265
|
+
messageTimestamp: editedTs
|
|
266
|
+
}
|
|
218
267
|
}
|
|
219
268
|
]);
|
|
220
|
-
return;
|
|
221
|
-
case 'markChatAsReadUpdate':
|
|
222
|
-
ev.emit('chats.update', [{ id: evt.jid, unreadCount: 0 }]);
|
|
223
|
-
return;
|
|
224
|
-
// ── Calls ──
|
|
225
|
-
case 'incomingCall': {
|
|
226
|
-
const isGroup = isJidGroup(evt.from);
|
|
227
|
-
// Map the canonical action onto upstream Baileys' `WACallUpdateType`.
|
|
228
|
-
// `preAccept` is upstream's `'ringing'`; `'timeout'` has no core
|
|
229
|
-
// equivalent (upstream synthesizes it from a separate signal).
|
|
230
|
-
const status = evt.action.type === 'preAccept' ? 'ringing' : evt.action.type;
|
|
231
|
-
const callEvt = {
|
|
232
|
-
chatId: evt.from,
|
|
233
|
-
from: evt.from,
|
|
234
|
-
id: evt.action.callId,
|
|
235
|
-
date: new Date(evt.timestamp * 1000),
|
|
236
|
-
status,
|
|
237
|
-
offline: evt.offline,
|
|
238
|
-
isGroup,
|
|
239
|
-
...(isGroup ? { groupJid: evt.from } : {}),
|
|
240
|
-
...(evt.action.type === 'offer' && evt.action.callerPn ? { callerPn: evt.action.callerPn } : {}),
|
|
241
|
-
...(evt.action.type === 'offer' ? { isVideo: !!evt.action.isVideo } : {})
|
|
242
|
-
};
|
|
243
|
-
ev.emit('call', [callEvt]);
|
|
244
|
-
return;
|
|
245
269
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
receipt: (evt, { ctx }) => {
|
|
273
|
+
// Fan out one MessageUserReceiptUpdate per id (upstream emits
|
|
274
|
+
// per-id) and pick the timestamp slot from the receipt type so
|
|
275
|
+
// consumers branching on `receipt.readTimestamp` /
|
|
276
|
+
// `receipt.playedTimestamp` see the right field populated.
|
|
277
|
+
const participant = evt.isGroup ? evt.senderJid : undefined;
|
|
278
|
+
const receipt = {};
|
|
279
|
+
if (evt.receiptType === 'read' || evt.receiptType === 'read-self') {
|
|
280
|
+
receipt.readTimestamp = evt.timestamp;
|
|
281
|
+
}
|
|
282
|
+
else if (evt.receiptType === 'played' || evt.receiptType === 'played-self') {
|
|
283
|
+
receipt.playedTimestamp = evt.timestamp;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
receipt.receiptTimestamp = evt.timestamp;
|
|
287
|
+
}
|
|
288
|
+
ctx.ev.emit('message-receipt.update', evt.messageIds.map(id => ({
|
|
289
|
+
key: { remoteJid: evt.chatJid, id, fromMe: evt.isFromMe, participant },
|
|
290
|
+
receipt
|
|
291
|
+
})));
|
|
292
|
+
},
|
|
293
|
+
undecryptableMessage: (evt, { ctx }) => {
|
|
294
|
+
// Mirror upstream `messages-recv.ts:1352-…`: surface a CIPHERTEXT
|
|
295
|
+
// stub on `messages.upsert` so consumers see the placeholder and
|
|
296
|
+
// can request a resend (PLACEHOLDER_MESSAGE_RESEND PDO). Logging
|
|
297
|
+
// happens at debug — upstream considers this routine.
|
|
298
|
+
ctx.logger.debug({ id: evt.id, chat: evt.chatJid, isUnavailable: evt.isUnavailable, fail: evt.decryptFailMode }, 'undecryptable message received');
|
|
299
|
+
// `decrypt_fail_mode === 'hide'` means the server told us to
|
|
300
|
+
// silently drop — match that by NOT emitting an upsert.
|
|
301
|
+
if (evt.decryptFailMode === 'hide')
|
|
302
|
+
return;
|
|
303
|
+
const stubMsg = WAProto.WebMessageInfo.fromObject({
|
|
304
|
+
key: {
|
|
305
|
+
remoteJid: evt.chatJid,
|
|
306
|
+
fromMe: evt.isFromMe,
|
|
307
|
+
id: evt.id,
|
|
308
|
+
participant: evt.senderJid
|
|
309
|
+
},
|
|
310
|
+
messageTimestamp: evt.timestamp,
|
|
311
|
+
pushName: evt.pushName,
|
|
312
|
+
messageStubType: WAProto.WebMessageInfo.StubType.CIPHERTEXT,
|
|
313
|
+
messageStubParameters: evt.unavailableType ? [evt.unavailableType] : []
|
|
314
|
+
});
|
|
315
|
+
if (evt.participantAlt)
|
|
316
|
+
stubMsg.key.participantAlt = evt.participantAlt;
|
|
317
|
+
if (evt.remoteJidAlt)
|
|
318
|
+
stubMsg.key.remoteJidAlt = evt.remoteJidAlt;
|
|
319
|
+
ctx.ev.emit('messages.upsert', { messages: [stubMsg], type: 'notify' });
|
|
320
|
+
},
|
|
321
|
+
// ── Contacts ──
|
|
322
|
+
pushNameUpdate: (evt, { ctx }) => ctx.ev.emit('contacts.update', [{ id: evt.jid, notify: evt.newPushName }]),
|
|
323
|
+
contactUpdate: (evt, { ctx }) => {
|
|
324
|
+
// Promote ContactAction fields into upstream's `Partial<Contact>`
|
|
325
|
+
// shape so consumers (sidebar UIs, contact pickers) see real names
|
|
326
|
+
// instead of bare ids. `verifiedName` is intentionally not set —
|
|
327
|
+
// that comes from a different notification path (business-level
|
|
328
|
+
// verification).
|
|
329
|
+
const update = { id: evt.jid };
|
|
330
|
+
const name = evt.fullName ?? evt.firstName;
|
|
331
|
+
if (name)
|
|
332
|
+
update.name = name;
|
|
333
|
+
if (evt.lidJid)
|
|
334
|
+
update.lid = evt.lidJid;
|
|
335
|
+
if (evt.pnJid)
|
|
336
|
+
update.phoneNumber = evt.pnJid;
|
|
337
|
+
ctx.ev.emit('contacts.update', [update]);
|
|
338
|
+
},
|
|
339
|
+
pictureUpdate: (evt, { ctx }) =>
|
|
340
|
+
// Upstream emits `imgUrl: null` on removal so consumers cache the
|
|
341
|
+
// absence instead of refetching forever. `'changed'` is the
|
|
342
|
+
// sentinel for "invalidate cache, refetch on demand".
|
|
343
|
+
ctx.ev.emit('contacts.update', [{ id: evt.jid, imgUrl: evt.removed ? null : 'changed' }]),
|
|
344
|
+
// ── Presence ──
|
|
345
|
+
presence: (evt, { ctx }) => ctx.ev.emit('presence.update', {
|
|
346
|
+
id: evt.from,
|
|
347
|
+
presences: {
|
|
348
|
+
[evt.from]: {
|
|
349
|
+
lastKnownPresence: (evt.unavailable ? 'unavailable' : 'available'),
|
|
350
|
+
lastSeen: evt.lastSeen
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}),
|
|
354
|
+
chatPresence: (evt, { ctx }) => {
|
|
355
|
+
// Bridge `ChatPresenceUpdate.state` is `'composing' | 'paused'`.
|
|
356
|
+
// `recording` is signaled via `media === 'audio'` riding on top of
|
|
357
|
+
// `composing` — upstream Baileys' `WAPresence` collapses the two
|
|
358
|
+
// signals into 'composing' / 'recording' / 'paused'. Anything
|
|
359
|
+
// outside this set falls back to `'paused'` (the safe sentinel —
|
|
360
|
+
// "stopped doing whatever they were doing") rather than passing
|
|
361
|
+
// through unvalidated, which previously could leak unknown wire
|
|
362
|
+
// values onto consumer-facing `WAPresence`.
|
|
363
|
+
let mapped;
|
|
364
|
+
if (evt.state === 'composing')
|
|
365
|
+
mapped = evt.media === 'audio' ? 'recording' : 'composing';
|
|
366
|
+
else if (evt.state === 'paused')
|
|
367
|
+
mapped = 'paused';
|
|
368
|
+
else {
|
|
369
|
+
ctx.logger.debug({ state: evt.state, media: evt.media }, 'chat_presence: unknown state — falling back to paused');
|
|
370
|
+
mapped = 'paused';
|
|
371
|
+
}
|
|
372
|
+
ctx.ev.emit('presence.update', {
|
|
373
|
+
id: evt.chatJid,
|
|
374
|
+
presences: { [evt.senderJid]: { lastKnownPresence: mapped } }
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
// ── Groups ──
|
|
378
|
+
groupUpdate: (evt, { ctx }) => {
|
|
379
|
+
const user = ctx.getUser();
|
|
380
|
+
const fromMe = !!(evt.author && (evt.author === user?.id || evt.author === user?.lid));
|
|
381
|
+
// Dispatch via discriminator instead of `as never` so TS validates
|
|
382
|
+
// payload ↔ event-name pairing.
|
|
383
|
+
const domainEvent = buildGroupNotificationDomainEvent(evt);
|
|
384
|
+
if (domainEvent) {
|
|
385
|
+
if (domainEvent.name === 'groups.update')
|
|
386
|
+
ctx.ev.emit('groups.update', domainEvent.payload);
|
|
387
|
+
else
|
|
388
|
+
ctx.ev.emit('group-participants.update', domainEvent.payload);
|
|
389
|
+
}
|
|
390
|
+
// `groups.upsert` is intentionally NOT emitted from `<create>`. The
|
|
391
|
+
// bridge gives us only the creation marker — `participants` /
|
|
392
|
+
// `subject` / `creation` are unknown without an extra IQ. Upstream
|
|
393
|
+
// emits this only AFTER `groupMetadata` resolves (`messages-recv.ts:661`),
|
|
394
|
+
// so emitting `[{ id }]` here would feed consumers a broken
|
|
395
|
+
// `GroupMetadata` (participants undefined → `metadata.participants.map`
|
|
396
|
+
// throws). Bots that want the metadata should call
|
|
397
|
+
// `sock.groupMetadata(evt.groupJid)` from a `groups.update` listener
|
|
398
|
+
// or after the matching `messages.upsert` stub.
|
|
399
|
+
// Fan out `group.join-request` for membership_approval_request /
|
|
400
|
+
// created_membership_requests / revoked_membership_requests.
|
|
401
|
+
// Upstream emits one event per affected participant.
|
|
402
|
+
for (const joinReq of buildGroupJoinRequestEvents(evt)) {
|
|
403
|
+
ctx.ev.emit('group.join-request', joinReq);
|
|
404
|
+
}
|
|
405
|
+
const stubMessages = buildGroupNotificationStubMessages(evt, fromMe);
|
|
406
|
+
if (stubMessages.length > 0) {
|
|
407
|
+
ctx.ev.emit('messages.upsert', { messages: stubMessages, type: 'notify' });
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
// ── Chat state ──
|
|
411
|
+
archiveUpdate: (evt, { ctx }) => ctx.ev.emit('chats.update', [{ id: evt.jid, archived: evt.archived }]),
|
|
412
|
+
pinUpdate: (evt, { ctx }) =>
|
|
413
|
+
// Upstream Baileys' `chat-utils.ts:878` emits `null` (not
|
|
414
|
+
// `undefined`) on unpin so consumers using `'pinned' in update`
|
|
415
|
+
// can distinguish "not touched" from "explicitly unpinned" —
|
|
416
|
+
// matters for ChatUpdate-shaped diffs where the property's
|
|
417
|
+
// presence is the signal.
|
|
418
|
+
ctx.ev.emit('chats.update', [{ id: evt.jid, pinned: evt.pinned ? evt.timestamp : null }]),
|
|
419
|
+
muteUpdate: (evt, { ctx }) =>
|
|
420
|
+
// Mirrors upstream `chat-utils.ts:809`: when muted, surface
|
|
421
|
+
// `muteEndTimestamp` (0 = forever); on unmute, surface `null`.
|
|
422
|
+
ctx.ev.emit('chats.update', [{ id: evt.jid, muteEndTime: evt.muted ? (evt.muteEndTimestamp ?? 0) : null }]),
|
|
423
|
+
starUpdate: (evt, { ctx }) => ctx.ev.emit('messages.update', [
|
|
424
|
+
{
|
|
425
|
+
key: { remoteJid: evt.chatJid, id: evt.messageId, fromMe: evt.fromMe, participant: evt.participantJid },
|
|
426
|
+
update: { starred: evt.starred }
|
|
427
|
+
}
|
|
428
|
+
]),
|
|
429
|
+
markChatAsReadUpdate: (evt, { ctx }) =>
|
|
430
|
+
// Mirrors upstream `chat-utils.ts:852`: read=true → unreadCount=0,
|
|
431
|
+
// read=false (mark as unread) → -1 sentinel.
|
|
432
|
+
ctx.ev.emit('chats.update', [{ id: evt.jid, unreadCount: evt.read ? 0 : -1 }]),
|
|
433
|
+
// ── Calls ──
|
|
434
|
+
incomingCall: (evt, { ctx }) => {
|
|
435
|
+
const isGroup = isJidGroup(evt.from);
|
|
436
|
+
// Map the canonical action onto upstream Baileys' WACallUpdateType.
|
|
437
|
+
// `preAccept` is upstream's `ringing`. `timeout` has no bridge
|
|
438
|
+
// counterpart yet (upstream synthesizes it from a separate signal).
|
|
439
|
+
const status = evt.action.type === 'preAccept' ? 'ringing' : evt.action.type;
|
|
440
|
+
const callEvt = {
|
|
441
|
+
chatId: evt.from,
|
|
442
|
+
from: evt.from,
|
|
443
|
+
id: evt.action.callId,
|
|
444
|
+
date: new Date(evt.timestamp * 1000),
|
|
445
|
+
status,
|
|
446
|
+
offline: evt.offline,
|
|
447
|
+
isGroup,
|
|
448
|
+
// Group JID = `evt.from` only when the stanza came from a group
|
|
449
|
+
// JID itself. The bridge offer payload doesn't carry a separate
|
|
450
|
+
// `group_jid` field today, so the group's JID IS the `from` for
|
|
451
|
+
// `<call>` stanzas in groups.
|
|
452
|
+
...(isGroup ? { groupJid: evt.from } : {}),
|
|
453
|
+
...(evt.action.callerPn ? { callerPn: evt.action.callerPn } : {}),
|
|
454
|
+
...(evt.action.type === 'offer' ? { isVideo: !!evt.action.isVideo } : {}),
|
|
455
|
+
// Bridge auxiliary fields (offer-only / terminate-only / always)
|
|
456
|
+
...(evt.action.callerCountryCode ? { callerCountryCode: evt.action.callerCountryCode } : {}),
|
|
457
|
+
...(evt.action.deviceClass ? { deviceClass: evt.action.deviceClass } : {}),
|
|
458
|
+
...(evt.action.joinable !== undefined ? { joinable: evt.action.joinable } : {}),
|
|
459
|
+
...(evt.action.audio ? { audio: evt.action.audio } : {}),
|
|
460
|
+
...(evt.action.duration !== undefined ? { duration: evt.action.duration } : {}),
|
|
461
|
+
...(evt.action.audioDuration !== undefined ? { audioDuration: evt.action.audioDuration } : {}),
|
|
462
|
+
...(evt.stanzaId ? { stanzaId: evt.stanzaId } : {}),
|
|
463
|
+
...(evt.notify ? { notify: evt.notify } : {}),
|
|
464
|
+
...(evt.platform ? { platform: evt.platform } : {}),
|
|
465
|
+
...(evt.version ? { version: evt.version } : {})
|
|
466
|
+
};
|
|
467
|
+
ctx.ev.emit('call', [callEvt]);
|
|
468
|
+
},
|
|
469
|
+
// ── Raw passthrough ──
|
|
470
|
+
rawNode: (evt, { ctx }) => emitCBEvents(ctx, evt.node),
|
|
471
|
+
notification: (evt, { ctx }) => ctx.logger.trace({ tag: evt.tag, attrs: evt.attrs }, 'bridge generic notification (no Baileys mapping)'),
|
|
472
|
+
mexNotification: (evt, { ctx }) => {
|
|
473
|
+
// Route by op_name. Adding a new MEX-driven event means a new entry
|
|
474
|
+
// here — no per-op_name plumbing in the Bridge layer.
|
|
475
|
+
if (evt.opName === 'NotificationUserReachoutTimelockUpdate') {
|
|
476
|
+
const state = mapReachoutTimelock(evt.payload);
|
|
477
|
+
if (state)
|
|
478
|
+
emitConnectionUpdate(ctx, { reachoutTimeLock: state });
|
|
479
|
+
else
|
|
480
|
+
ctx.logger.warn({ payload: evt.payload }, 'reachout-timelock push: payload missing expected fields');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
ctx.logger.trace({ opName: evt.opName, offline: evt.offline }, 'bridge mex notification with no Baileys mapping (drop)');
|
|
484
|
+
},
|
|
485
|
+
noop: (evt, { ctx }) => ctx.logger.trace({ bridgeType: evt.bridgeType, detail: evt.detail }, 'bridge event acknowledged (no Baileys equivalent)'),
|
|
486
|
+
lidMappingUpdate: (evt, { ctx }) => {
|
|
487
|
+
// Mirror upstream `messages-recv.ts:287`: one `lid-mapping.update`
|
|
488
|
+
// event per pair, payload is the bare `{ lid, pn }` shape.
|
|
489
|
+
for (const mapping of evt.mappings) {
|
|
490
|
+
ctx.ev.emit('lid-mapping.update', mapping);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
chatDelete: (evt, { ctx }) => ctx.ev.emit('chats.delete', [evt.jid]),
|
|
494
|
+
messageDelete: (evt, { ctx }) =>
|
|
495
|
+
// Upstream `chat-utils.ts:857` shape: `{ keys: WAMessageKey[] }` for
|
|
496
|
+
// per-message delete (not the `{ jid, all: true }` chat-clear case
|
|
497
|
+
// — that's a different sync action surface).
|
|
498
|
+
ctx.ev.emit('messages.delete', {
|
|
499
|
+
keys: [
|
|
500
|
+
{
|
|
501
|
+
remoteJid: evt.chatJid,
|
|
502
|
+
id: evt.messageId,
|
|
503
|
+
fromMe: evt.fromMe,
|
|
504
|
+
...(evt.participantJid ? { participant: evt.participantJid } : {})
|
|
505
|
+
}
|
|
506
|
+
]
|
|
507
|
+
}),
|
|
508
|
+
newsletterLiveUpdate: (evt, { ctx }) => {
|
|
509
|
+
// Fan out one `newsletter.reaction` per (message, reaction) pair —
|
|
510
|
+
// matches upstream `messages-recv.ts:320` semantics. Upstream sets
|
|
511
|
+
// `removed: true` when count is 0 (user removed their reaction);
|
|
512
|
+
// we mirror that.
|
|
513
|
+
for (const msg of evt.messages) {
|
|
514
|
+
for (const reaction of msg.reactions) {
|
|
515
|
+
ctx.ev.emit('newsletter.reaction', {
|
|
516
|
+
id: evt.newsletterJid,
|
|
517
|
+
server_id: msg.serverId,
|
|
518
|
+
reaction: {
|
|
519
|
+
code: reaction.code,
|
|
520
|
+
count: reaction.count,
|
|
521
|
+
removed: reaction.count === 0
|
|
263
522
|
}
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
ctx.logger.trace({ opName: evt.opName, offline: evt.offline }, 'bridge mex notification with no Baileys mapping (drop)');
|
|
267
|
-
return;
|
|
523
|
+
});
|
|
268
524
|
}
|
|
269
|
-
case 'noop':
|
|
270
|
-
ctx.logger.trace({ bridgeType: evt.bridgeType, detail: evt.detail }, 'bridge event acknowledged (no Baileys equivalent)');
|
|
271
|
-
return;
|
|
272
525
|
}
|
|
273
|
-
}
|
|
526
|
+
},
|
|
527
|
+
disappearingModeChanged: (evt, { ctx }) =>
|
|
528
|
+
// Upstream surfaces this through `chats.update.ephemeralExpiration`
|
|
529
|
+
// (set in `process-message.ts:417` for the protocolMessage path).
|
|
530
|
+
// Bridge gives us the same data via a dedicated event — emit on
|
|
531
|
+
// the same channel for parity.
|
|
532
|
+
ctx.ev.emit('chats.update', [
|
|
533
|
+
{
|
|
534
|
+
id: evt.jid,
|
|
535
|
+
ephemeralExpiration: evt.duration > 0 ? evt.duration : null,
|
|
536
|
+
ephemeralSettingTimestamp: evt.settingTimestamp
|
|
537
|
+
}
|
|
538
|
+
]),
|
|
539
|
+
historySync: (evt, { ctx }) => {
|
|
540
|
+
// 1:1 with upstream `process-message.ts:371-376`. `isLatest` is true
|
|
541
|
+
// when this is the first history sync the bot has seen since
|
|
542
|
+
// pairing — upstream tracks it in `creds.processedHistoryMessages`,
|
|
543
|
+
// but in baileyrs the bridge owns creds, so we approximate with the
|
|
544
|
+
// `INITIAL_BOOTSTRAP` syncType which only fires once per pair. Bots
|
|
545
|
+
// gating "first hydrate" logic on `isLatest` see the same boolean.
|
|
546
|
+
const HSType = WAProto.HistorySync.HistorySyncType;
|
|
547
|
+
const isInitial = evt.syncType === HSType.INITIAL_BOOTSTRAP;
|
|
548
|
+
const payload = {
|
|
549
|
+
chats: evt.chats,
|
|
550
|
+
contacts: evt.contacts,
|
|
551
|
+
messages: evt.messages,
|
|
552
|
+
isLatest: evt.syncType === HSType.ON_DEMAND ? undefined : isInitial,
|
|
553
|
+
progress: evt.progress,
|
|
554
|
+
syncType: evt.syncType,
|
|
555
|
+
chunkOrder: evt.chunkOrder,
|
|
556
|
+
peerDataRequestSessionId: evt.peerDataRequestSessionId,
|
|
557
|
+
lidPnMappings: evt.lidPnMappings.length > 0 ? evt.lidPnMappings : undefined
|
|
558
|
+
};
|
|
559
|
+
ctx.ev.emit('messaging-history.set', payload);
|
|
560
|
+
// Also fan out to `chats.upsert` / `contacts.upsert` so bots that
|
|
561
|
+
// wire those (instead of, or in addition to, messaging-history.set)
|
|
562
|
+
// get hydrated. Upstream emits both channels from different code
|
|
563
|
+
// paths; we collapse them here so consumers picking either pattern
|
|
564
|
+
// see the same data on the first pair.
|
|
565
|
+
if (evt.chats.length > 0)
|
|
566
|
+
ctx.ev.emit('chats.upsert', evt.chats);
|
|
567
|
+
if (evt.contacts.length > 0)
|
|
568
|
+
ctx.ev.emit('contacts.upsert', evt.contacts);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* Create the event handler that translates canonical events into Baileys
|
|
573
|
+
* events on `ctx.ev`. The handler does NO bridge-shape inspection — that's
|
|
574
|
+
* the schema's job. Field-access here references canonical types
|
|
575
|
+
* exclusively, so a downstream rename surfaces as a `tsc` failure.
|
|
576
|
+
*/
|
|
577
|
+
export const makeEventHandler = (ctx, callbacks) => {
|
|
578
|
+
const dispatchCtx = { ctx, callbacks };
|
|
274
579
|
return (event) => {
|
|
275
580
|
const canonical = adaptBridgeEvent(event, ctx.logger);
|
|
276
581
|
if (!canonical)
|
|
277
582
|
return;
|
|
278
|
-
|
|
583
|
+
// Type-safe table lookup. The runtime cast through `DispatcherFn`
|
|
584
|
+
// is a no-op TS-wise (the table is exhaustively typed) — needed
|
|
585
|
+
// only because TS can't follow the discriminator across the
|
|
586
|
+
// indexed-access into the mapped type.
|
|
587
|
+
const dispatcher = DISPATCHERS[canonical.type];
|
|
588
|
+
try {
|
|
589
|
+
dispatcher(canonical, dispatchCtx);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
// One bad event must not poison the rest of the pipeline.
|
|
593
|
+
ctx.logger.error({ err, type: canonical.type }, 'dispatcher threw — dropping event');
|
|
594
|
+
}
|
|
279
595
|
};
|
|
280
596
|
};
|
|
281
597
|
//# sourceMappingURL=events.js.map
|