@oxidezap/baileyrs 0.2.13 → 0.3.1

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.
@@ -28,229 +28,235 @@ const getNormalizedUserJid = (ctx) => {
28
28
  normalizedUserJids.set(ctx, { userId, jid });
29
29
  return jid;
30
30
  };
31
- export const makeMessageMethods = (ctx) => ({
32
- sendMessage: async (jid, content, options) => {
33
- const client = await ctx.getClient();
34
- const userJid = getNormalizedUserJid(ctx) ?? '';
35
- // SocketConfig contains transport/auth/cache state that message
36
- // generation neither reads nor owns. Copying all of it here retained a
37
- // large object graph and repeated dozens of property writes per send.
38
- // `options` is the one generation default shared with the socket; every
39
- // other generation knob belongs to this call's typed options.
40
- const generationOptions = {
41
- ...options,
42
- options: options?.options ?? ctx.fullConfig.options,
43
- logger: ctx.logger,
44
- userJid,
45
- waClient: client
46
- };
47
- const fullMsg = await generateWAMessage(jid, content, generationOptions);
48
- const msg = normalizeMessageContent(fullMsg.message);
49
- if (!msg)
50
- throw new Boom('Failed to generate message content', { statusCode: 400 });
51
- const contentType = getContentType(msg);
52
- if (contentType === 'protocolMessage') {
53
- const protoMsg = msg.protocolMessage;
54
- if (protoMsg?.type === WAProto.Message.ProtocolMessage.Type.REVOKE && protoMsg?.key?.id) {
55
- if (options?.messageId) {
56
- // The core has no revoke variant that takes a stanza id, so
57
- // the option cannot be honoured here. Saying so beats the
58
- // silent drop this option used to get on every path.
59
- ctx.logger.warn({ jid, messageId: options.messageId }, 'messageId option cannot be honoured for a revoke: the core assigns the stanza id itself');
31
+ export const makeMessageMethods = (ctx) => {
32
+ return {
33
+ sendMessage: async (jid, content, options) => {
34
+ return ctx.withClient(async (client) => {
35
+ const userJid = getNormalizedUserJid(ctx) ?? '';
36
+ // SocketConfig contains transport/auth/cache state that message
37
+ // generation neither reads nor owns. Copying all of it here retained a
38
+ // large object graph and repeated dozens of property writes per send.
39
+ // `options` is the one generation default shared with the socket; every
40
+ // other generation knob belongs to this call's typed options.
41
+ const generationOptions = {
42
+ ...options,
43
+ options: options?.options ?? ctx.fullConfig.options,
44
+ logger: ctx.logger,
45
+ userJid,
46
+ waClient: client
47
+ };
48
+ const fullMsg = await generateWAMessage(jid, content, generationOptions);
49
+ const msg = normalizeMessageContent(fullMsg.message);
50
+ if (!msg)
51
+ throw new Boom('Failed to generate message content', { statusCode: 400 });
52
+ const contentType = getContentType(msg);
53
+ if (contentType === 'protocolMessage') {
54
+ const protoMsg = msg.protocolMessage;
55
+ if (protoMsg?.type === WAProto.Message.ProtocolMessage.Type.REVOKE && protoMsg?.key?.id) {
56
+ if (options?.messageId) {
57
+ // The core has no revoke variant that takes a stanza id, so
58
+ // the option cannot be honoured here. Saying so beats the
59
+ // silent drop this option used to get on every path.
60
+ ctx.logger.warn({ jid, messageId: options.messageId }, 'messageId option cannot be honoured for a revoke: the core assigns the stanza id itself');
61
+ }
62
+ // Group revokes need the original sender's participant JID —
63
+ // without it the server rejects the revoke. The bridge
64
+ // signature is `revokeMessage(jid, message_id, participant?)`.
65
+ // Caller passes participant via `message.delete.participant`
66
+ // which lands on `protoMsg.key.participant` in the generated
67
+ // proto.
68
+ await client.revokeMessage(jid, protoMsg.key.id, protoMsg.key.participant ?? null);
69
+ return fullMsg;
70
+ }
71
+ if (protoMsg?.type === WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT &&
72
+ protoMsg?.key?.id &&
73
+ protoMsg?.editedMessage) {
74
+ const editBytes = WAProto.Message.encode(protoMsg.editedMessage).finish();
75
+ // Edit-path counterpart of `options.messageId` on a plain send, and
76
+ // deliberately not `resolveMessageId`: the engine treats any supplied
77
+ // stanza id as borrowed and binds no id-keyed state to it, so an edit
78
+ // nobody asked to pin would silently lose its retry-cache entry and
79
+ // outbound secret. Absent stays absent. `||` and not `??` because an
80
+ // empty id means unspecified everywhere else in this API.
81
+ const editStanzaId = options?.messageId || undefined;
82
+ const newMsgId = await client.editMessageBytes(jid, protoMsg.key.id, editBytes, editStanzaId);
83
+ fullMsg.key.id = newMsgId || fullMsg.key.id;
84
+ return fullMsg;
85
+ }
86
+ }
87
+ const msgBytes = encodeProtoCompat('Message', msg);
88
+ // The with-options sends are the same core send as the plain ones, with
89
+ // the stanza id supplied by the caller instead of drawn by the engine.
90
+ // The id resolves through the same place `relayMessage` resolves it.
91
+ const messageId = resolveMessageId(ctx.getUser(), options?.messageId);
92
+ const msgId = await sendReportingUpstreamFailure(() => jid === 'status@broadcast' && options?.statusJidList?.length
93
+ ? client.sendStatusMessageBytesWithOptions(msgBytes, options.statusJidList, messageId, EMPTY_RELAY_NODES, false)
94
+ : client.relayMessageBytesWithOptions(jid, msgBytes, messageId, EMPTY_RELAY_NODES, false, false));
95
+ fullMsg.key.id = msgId || fullMsg.key.id;
96
+ // Local echo of the message we just sent. Suppressed when
97
+ // `emitOwnEvents=false` so callers that explicitly opted out of seeing
98
+ // their own outbound messages don't get them looped back. This is the
99
+ // upstream-Baileys semantics of `emitOwnEvents` — it controls THIS
100
+ // echo, not inbound `fromMe` messages from other linked devices.
101
+ if (ctx.fullConfig.emitOwnEvents !== false) {
102
+ ctx.ev.emit('messages.upsert', {
103
+ messages: [fullMsg],
104
+ type: 'append'
105
+ });
60
106
  }
61
- // Group revokes need the original sender's participant JID —
62
- // without it the server rejects the revoke. The bridge
63
- // signature is `revokeMessage(jid, message_id, participant?)`.
64
- // Caller passes participant via `message.delete.participant`
65
- // which lands on `protoMsg.key.participant` in the generated
66
- // proto.
67
- await client.revokeMessage(jid, protoMsg.key.id, protoMsg.key.participant ?? null);
68
- return fullMsg;
69
- }
70
- if (protoMsg?.type === WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT &&
71
- protoMsg?.key?.id &&
72
- protoMsg?.editedMessage) {
73
- const editBytes = WAProto.Message.encode(protoMsg.editedMessage).finish();
74
- // Edit-path counterpart of `options.messageId` on a plain send, and
75
- // deliberately not `resolveMessageId`: the engine treats any supplied
76
- // stanza id as borrowed and binds no id-keyed state to it, so an edit
77
- // nobody asked to pin would silently lose its retry-cache entry and
78
- // outbound secret. Absent stays absent. `||` and not `??` because an
79
- // empty id means unspecified everywhere else in this API.
80
- const editStanzaId = options?.messageId || undefined;
81
- const newMsgId = await client.editMessageBytes(jid, protoMsg.key.id, editBytes, editStanzaId);
82
- fullMsg.key.id = newMsgId || fullMsg.key.id;
83
107
  return fullMsg;
84
- }
85
- }
86
- const msgBytes = encodeProtoCompat('Message', msg);
87
- // The with-options sends are the same core send as the plain ones, with
88
- // the stanza id supplied by the caller instead of drawn by the engine.
89
- // The id resolves through the same place `relayMessage` resolves it.
90
- const messageId = resolveMessageId(ctx.getUser(), options?.messageId);
91
- const msgId = await sendReportingUpstreamFailure(() => jid === 'status@broadcast' && options?.statusJidList?.length
92
- ? client.sendStatusMessageBytesWithOptions(msgBytes, options.statusJidList, messageId, EMPTY_RELAY_NODES, false)
93
- : client.relayMessageBytesWithOptions(jid, msgBytes, messageId, EMPTY_RELAY_NODES, false, false));
94
- fullMsg.key.id = msgId || fullMsg.key.id;
95
- // Local echo of the message we just sent. Suppressed when
96
- // `emitOwnEvents=false` so callers that explicitly opted out of seeing
97
- // their own outbound messages don't get them looped back. This is the
98
- // upstream-Baileys semantics of `emitOwnEvents` — it controls THIS
99
- // echo, not inbound `fromMe` messages from other linked devices.
100
- if (ctx.fullConfig.emitOwnEvents !== false) {
101
- ctx.ev.emit('messages.upsert', {
102
- messages: [fullMsg],
103
- type: 'append'
104
108
  });
105
- }
106
- return fullMsg;
107
- },
108
- updateMediaMessage: async (message) => {
109
- const client = await ctx.getClient();
110
- const content = normalizeMessageContent(message.message);
111
- const mediaContent = getMediaContent(content);
112
- if (!mediaContent) {
113
- throw new Boom('Not a media message', { statusCode: 400 });
114
- }
115
- const mediaKey = mediaContent.mediaKey;
116
- if (!mediaKey) {
117
- throw new Boom('Message has no media key', { statusCode: 400 });
118
- }
119
- const key = message.key;
120
- const newDirectPath = await client.requestMediaReupload(key.id, key.remoteJid, mediaKey instanceof Uint8Array ? mediaKey : new Uint8Array(mediaKey), !!key.fromMe, key.participant ?? null);
121
- // Update the message with the new direct path
122
- // (download uses directPath via Rust bridge, url is informational)
123
- mediaContent.directPath = newDirectPath;
124
- ctx.logger.debug({ directPath: newDirectPath, msgId: key.id }, 'media reupload successful');
125
- ctx.ev.emit('messages.update', [
126
- {
127
- key: message.key,
128
- update: { message: message.message }
109
+ },
110
+ updateMediaMessage: async (message) => {
111
+ return ctx.withClient(async (client) => {
112
+ const content = normalizeMessageContent(message.message);
113
+ const mediaContent = getMediaContent(content);
114
+ if (!mediaContent) {
115
+ throw new Boom('Not a media message', { statusCode: 400 });
116
+ }
117
+ const mediaKey = mediaContent.mediaKey;
118
+ if (!mediaKey) {
119
+ throw new Boom('Message has no media key', { statusCode: 400 });
120
+ }
121
+ const key = message.key;
122
+ const newDirectPath = await client.requestMediaReupload(key.id, key.remoteJid, mediaKey instanceof Uint8Array ? mediaKey : new Uint8Array(mediaKey), !!key.fromMe, key.participant ?? null);
123
+ // Update the message with the new direct path
124
+ // (download uses directPath via Rust bridge, url is informational)
125
+ mediaContent.directPath = newDirectPath;
126
+ ctx.logger.debug({ directPath: newDirectPath, msgId: key.id }, 'media reupload successful');
127
+ ctx.ev.emit('messages.update', [
128
+ {
129
+ key: message.key,
130
+ update: { message: message.message }
131
+ }
132
+ ]);
133
+ return message;
134
+ });
135
+ },
136
+ /**
137
+ * Low-level message relay — sends a raw proto.IMessage with full control
138
+ * over the message ID. Use `sendMessage` for the high-level API that handles
139
+ * media upload, message generation, link previews, etc.
140
+ *
141
+ * Returns the message ID used on the wire.
142
+ *
143
+ * @param jid Recipient JID
144
+ * @param message Raw protobuf Message (snake_case keys)
145
+ * @param options Relay options
146
+ */
147
+ relayMessage: async (jid, message, options) => {
148
+ return ctx.withClient(async (client) => {
149
+ const plan = planMessageRelay(jid, options, ctx.getUser());
150
+ ctx.logger.debug({
151
+ jid,
152
+ kind: plan.kind,
153
+ messageId: plan.messageId,
154
+ extraNodes: plan.kind === 'retransmission' ? 0 : plan.nodes.length
155
+ }, 'relayMessage compatibility plan');
156
+ // The message goes to the bridge as the caller built it: the core settles
157
+ // messageSecret / reportingTokenVersion itself, reusing a caller-set secret
158
+ // rather than replacing it, so nothing here has to be dropped.
159
+ const bytes = encodeProtoCompat('Message', message);
160
+ if (plan.kind === 'retransmission') {
161
+ await client.retransmitMessageBytes(jid, bytes, plan.input);
162
+ return plan.messageId;
163
+ }
164
+ // Both sends go through the same retry: a caller node the engine derives
165
+ // is refused the same way whether the message is bound for a chat or for
166
+ // status, and the escape hatch — a node it does not derive — survives
167
+ // either way.
168
+ const drop = (tag) => ctx.logger.debug({ jid, messageId: plan.messageId, tag }, 'dropped an additionalNodes entry the engine derives from the message');
169
+ if (plan.kind === 'status') {
170
+ return sendReportingUpstreamFailure(() => sendDroppingDerivedNodes(plan.nodes, nodes => client.sendStatusMessageBytesWithOptions(bytes, plan.recipients, plan.messageId, nodes, plan.refreshDevices), drop));
171
+ }
172
+ return sendReportingUpstreamFailure(() => sendDroppingDerivedNodes(plan.nodes, nodes => client.relayMessageBytesWithOptions(jid, bytes, plan.messageId, nodes, plan.refreshGroupMetadata, plan.refreshDevices), drop));
173
+ });
174
+ },
175
+ readMessages: async (keys) => {
176
+ const receiptKeys = receiptMessageKeys(keys);
177
+ if (receiptKeys.length)
178
+ await ctx.withClient(client => client.readMessages(receiptKeys));
179
+ },
180
+ /**
181
+ * Send a receipt for messages. The bridge handles most receipt types automatically
182
+ * (delivered, sender). Use `readMessages` for the common case of sending read receipts.
183
+ *
184
+ * Supported types via bridge: 'read', 'read-self', 'played'
185
+ * Auto-handled by bridge: 'sender', 'inactive', undefined (delivered)
186
+ * Not supported: 'hist_sync', 'peer_msg' (logged as warning)
187
+ */
188
+ sendReceipt: async (jid, participant, messageIds, type) => {
189
+ // Ahead of the empty-list exit: the types this method handles elsewhere
190
+ // resolve without sending anything, so a typo looked like a no-op.
191
+ assertArgumentDomain('sendReceipt', 'type', type, MESSAGE_RECEIPT_TYPES);
192
+ if (!messageIds.length)
193
+ return;
194
+ if (type === 'read' || type === 'read-self') {
195
+ const keys = messageIds.map(id => ({
196
+ remoteJid: jid,
197
+ id,
198
+ ...(participant ? { participant } : {})
199
+ }));
200
+ await ctx.withClient(client => client.readMessages(keys));
129
201
  }
130
- ]);
131
- return message;
132
- },
133
- /**
134
- * Low-level message relay — sends a raw proto.IMessage with full control
135
- * over the message ID. Use `sendMessage` for the high-level API that handles
136
- * media upload, message generation, link previews, etc.
137
- *
138
- * Returns the message ID used on the wire.
139
- *
140
- * @param jid Recipient JID
141
- * @param message Raw protobuf Message (snake_case keys)
142
- * @param options Relay options
143
- */
144
- relayMessage: async (jid, message, options) => {
145
- const client = await ctx.getClient();
146
- const plan = planMessageRelay(jid, options, ctx.getUser());
147
- ctx.logger.debug({
148
- jid,
149
- kind: plan.kind,
150
- messageId: plan.messageId,
151
- extraNodes: plan.kind === 'retransmission' ? 0 : plan.nodes.length
152
- }, 'relayMessage compatibility plan');
153
- // The message goes to the bridge as the caller built it: the core settles
154
- // messageSecret / reportingTokenVersion itself, reusing a caller-set secret
155
- // rather than replacing it, so nothing here has to be dropped.
156
- const bytes = encodeProtoCompat('Message', message);
157
- if (plan.kind === 'retransmission') {
158
- await client.retransmitMessageBytes(jid, bytes, plan.input);
159
- return plan.messageId;
160
- }
161
- // Both sends go through the same retry: a caller node the engine derives
162
- // is refused the same way whether the message is bound for a chat or for
163
- // status, and the escape hatch — a node it does not derive — survives
164
- // either way.
165
- const drop = (tag) => ctx.logger.debug({ jid, messageId: plan.messageId, tag }, 'dropped an additionalNodes entry the engine derives from the message');
166
- if (plan.kind === 'status') {
167
- return sendReportingUpstreamFailure(() => sendDroppingDerivedNodes(plan.nodes, nodes => client.sendStatusMessageBytesWithOptions(bytes, plan.recipients, plan.messageId, nodes, plan.refreshDevices), drop));
168
- }
169
- return sendReportingUpstreamFailure(() => sendDroppingDerivedNodes(plan.nodes, nodes => client.relayMessageBytesWithOptions(jid, bytes, plan.messageId, nodes, plan.refreshGroupMetadata, plan.refreshDevices), drop));
170
- },
171
- readMessages: async (keys) => {
172
- const receiptKeys = receiptMessageKeys(keys);
173
- if (receiptKeys.length)
174
- await (await ctx.getClient()).readMessages(receiptKeys);
175
- },
176
- /**
177
- * Send a receipt for messages. The bridge handles most receipt types automatically
178
- * (delivered, sender). Use `readMessages` for the common case of sending read receipts.
179
- *
180
- * Supported types via bridge: 'read', 'read-self', 'played'
181
- * Auto-handled by bridge: 'sender', 'inactive', undefined (delivered)
182
- * Not supported: 'hist_sync', 'peer_msg' (logged as warning)
183
- */
184
- sendReceipt: async (jid, participant, messageIds, type) => {
185
- // Ahead of the empty-list exit: the types this method handles elsewhere
186
- // resolve without sending anything, so a typo looked like a no-op.
187
- assertArgumentDomain('sendReceipt', 'type', type, MESSAGE_RECEIPT_TYPES);
188
- if (!messageIds.length)
189
- return;
190
- if (type === 'read' || type === 'read-self') {
191
- const keys = messageIds.map(id => ({
192
- remoteJid: jid,
193
- id,
194
- ...(participant ? { participant } : {})
195
- }));
196
- await (await ctx.getClient()).readMessages(keys);
197
- }
198
- else if (type === 'played') {
199
- // Voice/video-note played receipts. The bridge (and core) pick the wire
200
- // type (`played` vs `played-self` for newsletters) and the `participant`
201
- // attr from the chat jid, so we just hand over the keys — same shape as
202
- // readMessages.
203
- const keys = messageIds.map(id => ({
204
- remoteJid: jid,
205
- id,
206
- ...(participant ? { participant } : {})
207
- }));
208
- await (await ctx.getClient()).markPlayed(keys);
209
- }
210
- else {
211
- // delivered/sender/inactive receipts are sent automatically by the Rust bridge
212
- // hist_sync/peer_msg require bridge-side support
213
- ctx.logger.debug({ type, jid, count: messageIds.length }, 'sendReceipt: type handled automatically by bridge or not yet supported');
214
- }
215
- },
216
- /**
217
- * Send receipts for multiple message keys, grouped by JID and participant.
218
- */
219
- sendReceipts: async (keys, type) => {
220
- assertArgumentDomain('sendReceipts', 'type', type, MESSAGE_RECEIPT_TYPES);
221
- const client = await ctx.getClient();
222
- const receiptKeys = receiptMessageKeys(keys);
223
- if (type === 'read' || type === 'read-self') {
224
- if (receiptKeys.length) {
225
- await client.readMessages(receiptKeys);
202
+ else if (type === 'played') {
203
+ // Voice/video-note played receipts. The bridge (and core) pick the wire
204
+ // type (`played` vs `played-self` for newsletters) and the `participant`
205
+ // attr from the chat jid, so we just hand over the keys — same shape as
206
+ // readMessages.
207
+ const keys = messageIds.map(id => ({
208
+ remoteJid: jid,
209
+ id,
210
+ ...(participant ? { participant } : {})
211
+ }));
212
+ await ctx.withClient(client => client.markPlayed(keys));
226
213
  }
227
- }
228
- else if (type === 'played') {
229
- if (receiptKeys.length) {
230
- await client.markPlayed(receiptKeys);
214
+ else {
215
+ // delivered/sender/inactive receipts are sent automatically by the Rust bridge
216
+ // hist_sync/peer_msg require bridge-side support
217
+ ctx.logger.debug({ type, jid, count: messageIds.length }, 'sendReceipt: type handled automatically by bridge or not yet supported');
231
218
  }
219
+ },
220
+ /**
221
+ * Send receipts for multiple message keys, grouped by JID and participant.
222
+ */
223
+ sendReceipts: async (keys, type) => {
224
+ assertArgumentDomain('sendReceipts', 'type', type, MESSAGE_RECEIPT_TYPES);
225
+ return ctx.withClient(async (client) => {
226
+ const receiptKeys = receiptMessageKeys(keys);
227
+ if (type === 'read' || type === 'read-self') {
228
+ if (receiptKeys.length) {
229
+ await client.readMessages(receiptKeys);
230
+ }
231
+ }
232
+ else if (type === 'played') {
233
+ if (receiptKeys.length) {
234
+ await client.markPlayed(receiptKeys);
235
+ }
236
+ }
237
+ else {
238
+ ctx.logger.debug({ type, count: keys.length }, 'sendReceipts: type handled automatically by bridge or not yet supported');
239
+ }
240
+ });
241
+ },
242
+ /**
243
+ * Request resend of a placeholder message via PeerDataOperationRequest.
244
+ * Wraps the request in a protocolMessage and relays it to the chat.
245
+ */
246
+ requestPlaceholderResend: async (messageKey, msgData) => {
247
+ void msgData;
248
+ const message = {
249
+ protocolMessage: {
250
+ peerDataOperationRequestMessage: {
251
+ peerDataOperationRequestType: WAProto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND,
252
+ placeholderMessageResendRequest: [{ messageKey }]
253
+ },
254
+ type: WAProto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE
255
+ }
256
+ };
257
+ const bytes = encodeProtoCompat('Message', message);
258
+ return ctx.withClient(client => client.relayMessageBytes(messageKey.remoteJid, bytes, null));
232
259
  }
233
- else {
234
- ctx.logger.debug({ type, count: keys.length }, 'sendReceipts: type handled automatically by bridge or not yet supported');
235
- }
236
- },
237
- /**
238
- * Request resend of a placeholder message via PeerDataOperationRequest.
239
- * Wraps the request in a protocolMessage and relays it to the chat.
240
- */
241
- requestPlaceholderResend: async (messageKey, msgData) => {
242
- void msgData;
243
- const message = {
244
- protocolMessage: {
245
- peerDataOperationRequestMessage: {
246
- peerDataOperationRequestType: WAProto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND,
247
- placeholderMessageResendRequest: [{ messageKey }]
248
- },
249
- type: WAProto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_MESSAGE
250
- }
251
- };
252
- const bytes = encodeProtoCompat('Message', message);
253
- return (await ctx.getClient()).relayMessageBytes(messageKey.remoteJid, bytes, null);
254
- }
255
- });
260
+ };
261
+ };
256
262
  //# sourceMappingURL=messages.js.map