fb-messenger-e2ee 0.1.2 → 0.1.4

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/DOCS.md CHANGED
@@ -144,14 +144,52 @@ await client.sendReaction({
144
144
  });
145
145
  ```
146
146
 
147
- ### `unsendMessage(messageId: string, threadId?: string)`
147
+ ### `unsendMessage(input: UnsendMessageInput)`
148
148
 
149
- Un-sends/revokes an E2EE message you previously sent. Pass `threadId` unless the original send result is still in the short outbound cache.
149
+ Un-sends/revokes an E2EE message you previously sent.
150
+ You must provide the exact `messageId`.
151
+ Providing the `threadId` is highly recommended to ensure the E2EE client knows the destination JID.
150
152
 
151
153
  ```typescript
152
- await client.unsendMessage(sent.messageId, "1234567890.0@msgr");
154
+ await client.unsendMessage({
155
+ messageId: sent.messageId,
156
+ threadId: "1234567890.0@msgr"
157
+ });
153
158
  ```
154
159
 
160
+ **UnsendMessageInput**
161
+
162
+ | Field | Type | Description |
163
+ |---|---|---|
164
+ | `messageId` | `string` | The message ID to revoke. |
165
+ | `threadId` | `string` | Optional but highly recommended target thread identifier. |
166
+ | `fromMe` | `boolean` | Optional flag indicating if the message was sent by you (defaults to `true`). |
167
+
168
+ ### `editMessage(input: E2EEEditMessageInput)`
169
+
170
+ Edits the text of an E2EE message you previously sent.
171
+ You must provide the exact `messageId` (the 15-digit numeric ID returned by `sendMessage`) and the `threadId`.
172
+
173
+ ```typescript
174
+ const sent = await client.sendMessage({ threadId: "1234567890.0@msgr", text: "oops" });
175
+ const messageId = sent.messageId as string;
176
+
177
+ // ... a few seconds later
178
+ await client.editMessage({
179
+ threadId: "1234567890.0@msgr",
180
+ messageId,
181
+ newText: "corrected message",
182
+ });
183
+ ```
184
+
185
+ **E2EEEditMessageInput**
186
+
187
+ | Field | Type | Description |
188
+ |---|---|---|
189
+ | `threadId` | `string` | The thread ID (E2EE chat JID) containing the message. |
190
+ | `messageId` | `string` | The message ID to edit. |
191
+ | `newText` | `string` | The replacement text content. |
192
+
155
193
  ### `sendTyping(input: TypingInput)`
156
194
 
157
195
  Sends E2EE chatstate (`composing` / `paused`) over the Noise socket.
@@ -179,6 +217,47 @@ await client.sendImage({
179
217
  });
180
218
  ```
181
219
 
220
+ ### `sendFiles`
221
+
222
+ Sends multiple files/attachments in a single call.
223
+
224
+ - **Non-E2EE threads**: Bundles all files into a single Messenger message containing multiple attachments.
225
+ - **E2EE threads**: Since the Messenger E2EE transport protocol does not support multi-attachment payloads in a single encrypted frame, the attachments are sent sequentially as separate encrypted messages. Optional fields like `caption` and `replyToMessageId` are only applied to the first attachment.
226
+
227
+ Returns either a single result object (for non-E2EE) or an array of result objects (one per attachment for E2EE).
228
+
229
+ ```typescript
230
+ await client.sendFiles({
231
+ threadId: "1234567890.0@msgr",
232
+ caption: "Here are some files",
233
+ attachments: [
234
+ { data: imageBuffer, fileName: "photo.jpg" },
235
+ { data: pdfBuffer, fileName: "document.pdf" }
236
+ ]
237
+ });
238
+ ```
239
+
240
+ **SendMultipleMediaInput**
241
+
242
+ | Field | Type | Description |
243
+ |---|---|---|
244
+ | `threadId` | `string` | The target thread identifier. |
245
+ | `attachments` | `SendAttachmentItem[]` | An array of attachment objects to send. |
246
+ | `caption` | `string` | Optional caption. Applied to the bundled message for non-E2EE, or to the first attachment for E2EE. |
247
+ | `replyToMessageId` | `string` | Optional message ID to reply to. |
248
+
249
+ **SendAttachmentItem**
250
+
251
+ | Field | Type | Description |
252
+ |---|---|---|
253
+ | `data` | `Buffer` | The raw file bytes. |
254
+ | `fileName` | `string` | The filename. |
255
+ | `mimeType` | `string` | Optional MIME type (inferred from filename extension if omitted). |
256
+ | `width` | `number` | Optional width in pixels (for E2EE images/videos). |
257
+ | `height` | `number` | Optional height in pixels (for E2EE images/videos). |
258
+ | `seconds` | `number` | Optional duration in seconds (for E2EE videos/audios). |
259
+ | `ptt` | `boolean` | Optional flag indicating if audio should be sent as voice note (push-to-talk). |
260
+
182
261
  ---
183
262
 
184
263
  ## Event Handling
package/dist/index.cjs CHANGED
@@ -3472,6 +3472,30 @@ var FacebookGatewayService = class {
3472
3472
  );
3473
3473
  return response ?? {};
3474
3474
  }
3475
+ /**
3476
+ * Send multiple attachments in a single FCA message.
3477
+ * FCA-unofficial accepts an array in `attachment` field, which bundles all
3478
+ * files into one Messenger message on the wire.
3479
+ */
3480
+ async sendMultipleAttachmentsMessage(api, input) {
3481
+ if (input.attachments.length === 0) {
3482
+ throw new Error("sendMultipleAttachmentsMessage requires at least one attachment");
3483
+ }
3484
+ const streams = input.attachments.map(({ data, fileName }) => {
3485
+ const stream = import_node_stream.Readable.from(data);
3486
+ Object.assign(stream, { path: fileName });
3487
+ return stream;
3488
+ });
3489
+ const payload = {
3490
+ body: input.caption ?? "",
3491
+ // FCA-unofficial accepts a single Readable or an array of Readables
3492
+ attachment: streams.length === 1 ? streams[0] : streams
3493
+ };
3494
+ const response = await Promise.resolve(
3495
+ api.sendMessage(payload, input.threadId, void 0, input.replyToMessageId)
3496
+ );
3497
+ return response ?? {};
3498
+ }
3475
3499
  async sendReaction(api, messageId, reaction) {
3476
3500
  if (!api.setMessageReaction) {
3477
3501
  throw new Error("setMessageReaction is not available in fca-unofficial");
@@ -3648,6 +3672,13 @@ var MediaService = class {
3648
3672
  replyToMessageId: input.replyToMessageId
3649
3673
  });
3650
3674
  }
3675
+ /**
3676
+ * Send multiple attachments bundled into a single FCA message.
3677
+ * Non-E2EE only — for E2EE threads use the controller which sends them sequentially.
3678
+ */
3679
+ async sendFiles(api, input) {
3680
+ return this.gateway.sendMultipleAttachmentsMessage(api, input);
3681
+ }
3651
3682
  async sendSticker(api, input) {
3652
3683
  return this.gateway.sendStickerMessage(api, {
3653
3684
  threadId: input.threadId,
@@ -8175,12 +8206,13 @@ var ClientController = class {
8175
8206
  const isE2EE = this.isE2EEThreadId(input.threadId);
8176
8207
  const isGroup = input.threadId.includes("@g.us") || input.threadId.includes(".g.");
8177
8208
  if (this.e2eeConnected && isE2EE) {
8209
+ let messageId;
8178
8210
  if (isGroup) {
8179
- await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8211
+ messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8180
8212
  } else {
8181
- await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8213
+ messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8182
8214
  }
8183
- return { messageId: `e2ee-${now()}`, timestampMs: now() };
8215
+ return { messageId, timestampMs: now() };
8184
8216
  }
8185
8217
  return this.messagingService.sendText(this.requireApi(), input);
8186
8218
  }
@@ -8240,6 +8272,7 @@ var ClientController = class {
8240
8272
  createdAtMs: now()
8241
8273
  });
8242
8274
  logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
8275
+ return messageId;
8243
8276
  }
8244
8277
  async sendE2EEGroupText(groupJid, text, replyToMessageId) {
8245
8278
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
@@ -8302,6 +8335,7 @@ var ClientController = class {
8302
8335
  createdAtMs: now()
8303
8336
  });
8304
8337
  logger.info("ClientController", `E2EE Group message sent to ${groupJid} with ${participantNodes.length} devices`);
8338
+ return messageId;
8305
8339
  }
8306
8340
  getSelfE2EEJid() {
8307
8341
  const device = this.activeDeviceStore?.jidDevice ?? 0;
@@ -8313,8 +8347,210 @@ var ClientController = class {
8313
8347
  async sendReaction(input) {
8314
8348
  await this.messagingService.react(this.requireApi(), input);
8315
8349
  }
8316
- async unsendMessage(messageId) {
8317
- await this.messagingService.unsend(this.requireApi(), messageId);
8350
+ /**
8351
+ * Unsend/revoke a message.
8352
+ *
8353
+ * - **E2EE threads**: Sends an encrypted `ConsumerApplication { applicationData { revoke } }`
8354
+ * message over the Noise socket. The `fromMe` flag in the revoke key determines
8355
+ * whether it is a sender revoke (`true`, default) or an admin revoke (`false`).
8356
+ * - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP unsend.
8357
+ */
8358
+ async unsendMessage(input) {
8359
+ const isE2EE = this.isE2EEThreadId(input.threadId);
8360
+ if (this.e2eeConnected && isE2EE) {
8361
+ await this.sendE2EERevoke(input.threadId, input.messageId, input.fromMe ?? true);
8362
+ return;
8363
+ }
8364
+ await this.messagingService.unsend(this.requireApi(), input.messageId);
8365
+ }
8366
+ /**
8367
+ * Send an E2EE revoke (unsend) for a DM or group message.
8368
+ *
8369
+ * Builds a `ConsumerApplication { applicationData { revoke { key { id, fromMe } } } }`
8370
+ * payload, wraps it in MessageApplication + MessageTransport, then fans out to all
8371
+ * participant devices exactly like a normal DM or group text — but with the
8372
+ * correct edit attribute on the `<message>` node so the server and
8373
+ * receiving clients apply the correct revoke semantics.
8374
+ */
8375
+ async sendE2EERevoke(threadId, messageId, fromMe) {
8376
+ if (!this.e2eeSocket) throw new Error("E2EE not connected");
8377
+ const e2eeClient = this.e2eeService.getClient();
8378
+ const selfJid = this.getSelfE2EEJid();
8379
+ const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
8380
+ const editAttr = fromMe ? "7" : "8";
8381
+ const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
8382
+ const consumerApp = e2eeClient.buildRevokeMessage(messageId, { fromMe, remoteJid: toJid });
8383
+ const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
8384
+ const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
8385
+ if (isGroup) {
8386
+ const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
8387
+ const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
8388
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
8389
+ const groupResult = await e2eeClient.encryptGroupMessageApplication(
8390
+ threadId,
8391
+ selfJid,
8392
+ messageApp,
8393
+ newMessageId
8394
+ );
8395
+ const participantNodes = [];
8396
+ for (const deviceJid of deviceJids) {
8397
+ try {
8398
+ if (!await e2eeClient.hasSession(deviceJid)) {
8399
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8400
+ await e2eeClient.establishSession(deviceJid, bundle);
8401
+ }
8402
+ const payload = sameMessengerUser(deviceJid, selfJid) ? groupResult.selfDevicePayload : groupResult.devicePayload;
8403
+ const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8404
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8405
+ encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
8406
+ ]));
8407
+ } catch (err) {
8408
+ logger.error("ClientController", `Failed to distribute revoke SKDM to ${deviceJid}:`, err);
8409
+ }
8410
+ }
8411
+ const phash = buildParticipantListHash(deviceJids);
8412
+ const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
8413
+ encodeNode("participants", {}, participantNodes),
8414
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8415
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))]),
8416
+ encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
8417
+ ]);
8418
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8419
+ logger.info("ClientController", `E2EE group revoke sent for message ${messageId} in ${threadId}`);
8420
+ } else {
8421
+ const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8422
+ const selfDevicePayload = e2eeClient.buildMessageTransport({
8423
+ messageApp,
8424
+ dsm: { destinationJid: toJid, phash: "" }
8425
+ });
8426
+ const participantNodes = [];
8427
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
8428
+ for (const deviceJid of deviceJids) {
8429
+ if (sameMessengerDevice(deviceJid, selfJid)) continue;
8430
+ try {
8431
+ if (!await e2eeClient.hasSession(deviceJid)) {
8432
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8433
+ await e2eeClient.establishSession(deviceJid, bundle);
8434
+ }
8435
+ const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
8436
+ const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8437
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8438
+ encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
8439
+ ]));
8440
+ } catch (err) {
8441
+ logger.error("ClientController", `Failed to encrypt revoke to ${deviceJid}:`, err);
8442
+ }
8443
+ }
8444
+ const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
8445
+ encodeNode("participants", {}, participantNodes),
8446
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8447
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))])
8448
+ ]);
8449
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8450
+ logger.info("ClientController", `E2EE DM revoke sent for message ${messageId} to ${toJid}`);
8451
+ }
8452
+ }
8453
+ /**
8454
+ * Edit an E2EE message (change its text).
8455
+ *
8456
+ * - **E2EE threads**: Sends an encrypted `ConsumerApplication { content { editMessage { key, message, timestampMS } } }`
8457
+ * payload with the edit message attribute set.
8458
+ * - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP edit.
8459
+ */
8460
+ async editMessage(input) {
8461
+ const isE2EE = this.isE2EEThreadId(input.threadId);
8462
+ if (this.e2eeConnected && isE2EE) {
8463
+ await this.sendE2EEEdit(input.threadId, input.messageId, input.newText);
8464
+ return;
8465
+ }
8466
+ await this.threadService.editMessage(this.requireApi(), {
8467
+ messageId: input.messageId,
8468
+ newText: input.newText
8469
+ });
8470
+ }
8471
+ /**
8472
+ * Send an E2EE message edit for a DM or group message.
8473
+ *
8474
+ * Builds a `ConsumerApplication { payload { content { editMessage { key, message, timestampMS } } } }`
8475
+ * payload, fanned out to all participant devices exactly like a normal message send.
8476
+ */
8477
+ async sendE2EEEdit(threadId, messageId, newText) {
8478
+ if (!this.e2eeSocket) throw new Error("E2EE not connected");
8479
+ const e2eeClient = this.e2eeService.getClient();
8480
+ const selfJid = this.getSelfE2EEJid();
8481
+ const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
8482
+ const editAttr = "1";
8483
+ const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
8484
+ const consumerApp = e2eeClient.buildEditMessage(messageId, newText);
8485
+ const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
8486
+ const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
8487
+ if (isGroup) {
8488
+ const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
8489
+ const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
8490
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
8491
+ const groupResult = await e2eeClient.encryptGroupMessageApplication(
8492
+ threadId,
8493
+ selfJid,
8494
+ messageApp,
8495
+ newMessageId
8496
+ );
8497
+ const participantNodes = [];
8498
+ for (const deviceJid of deviceJids) {
8499
+ try {
8500
+ if (!await e2eeClient.hasSession(deviceJid)) {
8501
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8502
+ await e2eeClient.establishSession(deviceJid, bundle);
8503
+ }
8504
+ const payload = sameMessengerUser(deviceJid, selfJid) ? groupResult.selfDevicePayload : groupResult.devicePayload;
8505
+ const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8506
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8507
+ encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
8508
+ ]));
8509
+ } catch (err) {
8510
+ logger.error("ClientController", `Failed to distribute edit SKDM to ${deviceJid}:`, err);
8511
+ }
8512
+ }
8513
+ const phash = buildParticipantListHash(deviceJids);
8514
+ const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
8515
+ encodeNode("participants", {}, participantNodes),
8516
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8517
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))]),
8518
+ encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
8519
+ ]);
8520
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8521
+ logger.info("ClientController", `E2EE group edit sent for message ${messageId} in ${threadId}`);
8522
+ } else {
8523
+ const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8524
+ const selfDevicePayload = e2eeClient.buildMessageTransport({
8525
+ messageApp,
8526
+ dsm: { destinationJid: toJid, phash: "" }
8527
+ });
8528
+ const participantNodes = [];
8529
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
8530
+ for (const deviceJid of deviceJids) {
8531
+ if (sameMessengerDevice(deviceJid, selfJid)) continue;
8532
+ try {
8533
+ if (!await e2eeClient.hasSession(deviceJid)) {
8534
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8535
+ await e2eeClient.establishSession(deviceJid, bundle);
8536
+ }
8537
+ const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
8538
+ const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8539
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8540
+ encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
8541
+ ]));
8542
+ } catch (err) {
8543
+ logger.error("ClientController", `Failed to encrypt edit to ${deviceJid}:`, err);
8544
+ }
8545
+ }
8546
+ const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
8547
+ encodeNode("participants", {}, participantNodes),
8548
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8549
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))])
8550
+ ]);
8551
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8552
+ logger.info("ClientController", `E2EE DM edit sent for message ${messageId} to ${toJid}`);
8553
+ }
8318
8554
  }
8319
8555
  async sendTyping(input) {
8320
8556
  await this.messagingService.sendTyping(this.requireApi(), input);
@@ -8322,7 +8558,7 @@ var ClientController = class {
8322
8558
  async markAsRead(input) {
8323
8559
  await this.messagingService.markAsRead(this.requireApi(), input);
8324
8560
  }
8325
- // --- E2EE Media Upload Config ---
8561
+ // E2EE Media Upload Config
8326
8562
  async getE2EEMediaUploadConfig() {
8327
8563
  if (this.e2eeUploadConfig && !this.isMediaUploadConfigExpired(this.e2eeUploadConfig)) {
8328
8564
  return this.e2eeUploadConfig;
@@ -8469,6 +8705,62 @@ var ClientController = class {
8469
8705
  }
8470
8706
  return this.mediaService.sendFile(this.requireApi(), input);
8471
8707
  }
8708
+ /**
8709
+ * Send multiple files/attachments.
8710
+ *
8711
+ * - **Non-E2EE threads**: All files are bundled into a single FCA message.
8712
+ * - **E2EE threads**: Files are sent as separate sequential encrypted E2EE
8713
+ * media messages (the protocol does not support multi-attachment in one
8714
+ * encrypted frame). Returns an array with one result per attachment.
8715
+ */
8716
+ async sendFiles(input) {
8717
+ if (input.attachments.length === 0) {
8718
+ throw new Error("sendFiles requires at least one attachment");
8719
+ }
8720
+ if (this.e2eeConnected && this.isE2EEThreadId(input.threadId)) {
8721
+ const results = [];
8722
+ for (let i = 0; i < input.attachments.length; i++) {
8723
+ const att = input.attachments[i];
8724
+ const mediaInput = {
8725
+ threadId: input.threadId,
8726
+ data: att.data,
8727
+ fileName: att.fileName,
8728
+ mimeType: att.mimeType,
8729
+ // Caption on first attachment only
8730
+ caption: i === 0 ? input.caption : void 0,
8731
+ replyToMessageId: i === 0 ? input.replyToMessageId : void 0,
8732
+ width: att.width,
8733
+ height: att.height,
8734
+ seconds: att.seconds,
8735
+ ptt: att.ptt
8736
+ };
8737
+ const mediaType = inferMediaType(att.fileName, att.mimeType);
8738
+ let result;
8739
+ switch (mediaType) {
8740
+ case "image":
8741
+ result = await this.sendE2EEImage(mediaInput);
8742
+ break;
8743
+ case "video":
8744
+ result = await this.sendE2EEVideo(mediaInput);
8745
+ break;
8746
+ case "audio":
8747
+ result = await this.sendE2EEAudio(mediaInput);
8748
+ break;
8749
+ default:
8750
+ result = await this.sendE2EEFile(mediaInput);
8751
+ break;
8752
+ }
8753
+ results.push(result);
8754
+ }
8755
+ return results;
8756
+ }
8757
+ return this.mediaService.sendFiles(this.requireApi(), {
8758
+ threadId: input.threadId,
8759
+ attachments: input.attachments.map((a) => ({ data: a.data, fileName: a.fileName })),
8760
+ caption: input.caption,
8761
+ replyToMessageId: input.replyToMessageId
8762
+ });
8763
+ }
8472
8764
  async sendSticker(input) {
8473
8765
  return this.mediaService.sendSticker(this.requireApi(), input);
8474
8766
  }
@@ -8508,9 +8800,6 @@ var ClientController = class {
8508
8800
  async createPoll(input) {
8509
8801
  await this.threadService.createPoll(this.requireApi(), input);
8510
8802
  }
8511
- async editMessage(input) {
8512
- return this.threadService.editMessage(this.requireApi(), input);
8513
- }
8514
8803
  async addGroupMember(input) {
8515
8804
  await this.threadService.addGroupMember(this.requireApi(), input);
8516
8805
  }
@@ -8528,6 +8817,17 @@ var ClientController = class {
8528
8817
  return this.api;
8529
8818
  }
8530
8819
  };
8820
+ function inferMediaType(fileName, mimeType) {
8821
+ const mime = (mimeType ?? "").toLowerCase();
8822
+ if (mime.startsWith("image/")) return "image";
8823
+ if (mime.startsWith("video/")) return "video";
8824
+ if (mime.startsWith("audio/")) return "audio";
8825
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
8826
+ if (["jpg", "jpeg", "png", "gif", "webp", "heic", "heif", "bmp"].includes(ext)) return "image";
8827
+ if (["mp4", "mov", "avi", "mkv", "webm", "3gp", "m4v"].includes(ext)) return "video";
8828
+ if (["mp3", "ogg", "aac", "flac", "wav", "m4a", "opus"].includes(ext)) return "audio";
8829
+ return "document";
8830
+ }
8531
8831
 
8532
8832
  // src/repositories/session.repository.ts
8533
8833
  init_cjs_shims();
@@ -8767,8 +9067,11 @@ var FBClient = class {
8767
9067
  async sendReaction(input) {
8768
9068
  await this.controller.sendReaction(input);
8769
9069
  }
8770
- async unsendMessage(messageId) {
8771
- await this.controller.unsendMessage(messageId);
9070
+ async unsendMessage(input) {
9071
+ await this.controller.unsendMessage(input);
9072
+ }
9073
+ async editMessage(input) {
9074
+ await this.controller.editMessage(input);
8772
9075
  }
8773
9076
  async sendTyping(input) {
8774
9077
  await this.controller.sendTyping(input);
@@ -8786,6 +9089,17 @@ var FBClient = class {
8786
9089
  async sendFile(input) {
8787
9090
  return this.controller.sendFile(input);
8788
9091
  }
9092
+ /**
9093
+ * Send multiple files/attachments in one call.
9094
+ *
9095
+ * - **Non-E2EE threads**: All attachments land in a single Messenger message.
9096
+ * - **E2EE threads**: Each attachment is sent as a separate encrypted message
9097
+ * (E2EE does not support multi-attachment in one frame). Returns an array
9098
+ * with one result per attachment.
9099
+ */
9100
+ async sendFiles(input) {
9101
+ return this.controller.sendFiles(input);
9102
+ }
8789
9103
  };
8790
9104
  // Annotate the CommonJS export names for ESM import in node:
8791
9105
  0 && (module.exports = {