fb-messenger-e2ee 0.1.3 → 0.1.5

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
@@ -217,6 +217,47 @@ await client.sendImage({
217
217
  });
218
218
  ```
219
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
+
220
261
  ---
221
262
 
222
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,
@@ -3945,8 +3976,8 @@ init_cjs_shims();
3945
3976
  var MessageBuilder = class {
3946
3977
  content;
3947
3978
  replyTo;
3948
- setReply(id, senderJid) {
3949
- this.replyTo = { id, senderJid };
3979
+ setReply(replyTo) {
3980
+ this.replyTo = replyTo;
3950
3981
  return this;
3951
3982
  }
3952
3983
  getReply() {
@@ -4170,7 +4201,7 @@ function encodeMessageApplication(consumerAppBytes, replyTo) {
4170
4201
  const appPayload = new ProtoWriter().bytes(4, payloadSubProto).build();
4171
4202
  let metadataWriter = new ProtoWriter().bytes(8, frankingKey).varint(9, 0);
4172
4203
  if (replyTo) {
4173
- const quoted = new ProtoWriter().string(1, replyTo.id).string(2, replyTo.senderJid).build();
4204
+ const quoted = new ProtoWriter().string(1, replyTo.messageId).string(2, replyTo.chatJid).string(3, replyTo.senderJid).build();
4174
4205
  metadataWriter = metadataWriter.bytes(10, quoted);
4175
4206
  }
4176
4207
  const metadata = metadataWriter.build();
@@ -5542,7 +5573,16 @@ var E2EEClient = class {
5542
5573
  async buildDMTextFanoutPayloads(opts) {
5543
5574
  const builder = new MessageBuilder().setText(opts.text);
5544
5575
  if (opts.replyToId && opts.replyToSenderJid) {
5545
- builder.setReply(opts.replyToId, opts.replyToSenderJid);
5576
+ const replyTo = {
5577
+ messageId: opts.replyToId,
5578
+ // chatJid = remoteJID: the thread/chat that holds the quoted message
5579
+ // For DM, this is the peer's bare JID (toJid).
5580
+ chatJid: opts.toJid,
5581
+ // senderJid = participant: who sent the original message.
5582
+ // Caller must supply the correct sender JID via replyToSenderJid.
5583
+ senderJid: opts.replyToSenderJid
5584
+ };
5585
+ builder.setReply(replyTo);
5546
5586
  }
5547
5587
  const consumerApp = builder.build();
5548
5588
  const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
@@ -5577,7 +5617,14 @@ var E2EEClient = class {
5577
5617
  async encryptGroupText(groupJid, selfJid, text, messageId, replyToId, replyToSenderJid) {
5578
5618
  const builder = new MessageBuilder().setText(text);
5579
5619
  if (replyToId && replyToSenderJid) {
5580
- builder.setReply(replyToId, replyToSenderJid);
5620
+ const replyTo = {
5621
+ messageId: replyToId,
5622
+ // chatJid = remoteJID: for a group message, this is the group JID
5623
+ chatJid: groupJid,
5624
+ // participant: the specific member who sent the original message
5625
+ senderJid: replyToSenderJid
5626
+ };
5627
+ builder.setReply(replyTo);
5581
5628
  }
5582
5629
  const consumerApp = builder.build();
5583
5630
  const { messageApp, frankingTag } = encodeMessageApplication(consumerApp, builder.getReply());
@@ -8177,15 +8224,25 @@ var ClientController = class {
8177
8224
  if (this.e2eeConnected && isE2EE) {
8178
8225
  let messageId;
8179
8226
  if (isGroup) {
8180
- messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8227
+ messageId = await this.sendE2EEGroupText(
8228
+ input.threadId,
8229
+ input.text,
8230
+ input.replyToMessageId,
8231
+ input.replyToSenderJid
8232
+ );
8181
8233
  } else {
8182
- messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8234
+ messageId = await this.sendE2EEText(
8235
+ input.threadId,
8236
+ input.text,
8237
+ input.replyToMessageId,
8238
+ input.replyToSenderJid
8239
+ );
8183
8240
  }
8184
8241
  return { messageId, timestampMs: now() };
8185
8242
  }
8186
8243
  return this.messagingService.sendText(this.requireApi(), input);
8187
8244
  }
8188
- async sendE2EEText(threadId, text, replyToMessageId) {
8245
+ async sendE2EEText(threadId, text, replyToMessageId, replyToSenderJid) {
8189
8246
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
8190
8247
  const e2eeClient = this.e2eeService.getClient();
8191
8248
  const selfJid = this.getSelfE2EEJid();
@@ -8197,7 +8254,9 @@ var ClientController = class {
8197
8254
  text,
8198
8255
  isGroup: false,
8199
8256
  replyToId: replyToMessageId,
8200
- replyToSenderJid: replyToMessageId ? toJid : void 0
8257
+ // For DM: participant = sender of original msg. If caller knows who sent it, use that;
8258
+ // otherwise fall back to the peer's JID (correct for messages they sent to us).
8259
+ replyToSenderJid: replyToMessageId ? replyToSenderJid ?? toJid : void 0
8201
8260
  });
8202
8261
  const participantNodes = [];
8203
8262
  const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
@@ -8243,7 +8302,7 @@ var ClientController = class {
8243
8302
  logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
8244
8303
  return messageId;
8245
8304
  }
8246
- async sendE2EEGroupText(groupJid, text, replyToMessageId) {
8305
+ async sendE2EEGroupText(groupJid, text, replyToMessageId, replyToSenderJid) {
8247
8306
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
8248
8307
  const e2eeClient = this.e2eeService.getClient();
8249
8308
  const selfJid = this.getSelfE2EEJid();
@@ -8259,7 +8318,8 @@ var ClientController = class {
8259
8318
  text,
8260
8319
  messageId,
8261
8320
  replyToMessageId,
8262
- void 0
8321
+ replyToSenderJid
8322
+ // pass through — undefined when not replying
8263
8323
  );
8264
8324
  const participantNodes = [];
8265
8325
  for (const deviceJid of deviceJids) {
@@ -8527,7 +8587,7 @@ var ClientController = class {
8527
8587
  async markAsRead(input) {
8528
8588
  await this.messagingService.markAsRead(this.requireApi(), input);
8529
8589
  }
8530
- // --- E2EE Media Upload Config ---
8590
+ // E2EE Media Upload Config
8531
8591
  async getE2EEMediaUploadConfig() {
8532
8592
  if (this.e2eeUploadConfig && !this.isMediaUploadConfigExpired(this.e2eeUploadConfig)) {
8533
8593
  return this.e2eeUploadConfig;
@@ -8599,7 +8659,11 @@ var ClientController = class {
8599
8659
  const consumerApp = buildMessage(media.mediaFields);
8600
8660
  const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(
8601
8661
  consumerApp,
8602
- input.replyToMessageId ? { id: input.replyToMessageId, senderJid: toJid } : void 0
8662
+ input.replyToMessageId ? {
8663
+ messageId: input.replyToMessageId,
8664
+ chatJid: toJid,
8665
+ senderJid: input.replyToSenderJid ?? toJid
8666
+ } : void 0
8603
8667
  );
8604
8668
  const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8605
8669
  const selfDevicePayload = e2eeClient.buildMessageTransport({
@@ -8674,6 +8738,62 @@ var ClientController = class {
8674
8738
  }
8675
8739
  return this.mediaService.sendFile(this.requireApi(), input);
8676
8740
  }
8741
+ /**
8742
+ * Send multiple files/attachments.
8743
+ *
8744
+ * - **Non-E2EE threads**: All files are bundled into a single FCA message.
8745
+ * - **E2EE threads**: Files are sent as separate sequential encrypted E2EE
8746
+ * media messages (the protocol does not support multi-attachment in one
8747
+ * encrypted frame). Returns an array with one result per attachment.
8748
+ */
8749
+ async sendFiles(input) {
8750
+ if (input.attachments.length === 0) {
8751
+ throw new Error("sendFiles requires at least one attachment");
8752
+ }
8753
+ if (this.e2eeConnected && this.isE2EEThreadId(input.threadId)) {
8754
+ const results = [];
8755
+ for (let i = 0; i < input.attachments.length; i++) {
8756
+ const att = input.attachments[i];
8757
+ const mediaInput = {
8758
+ threadId: input.threadId,
8759
+ data: att.data,
8760
+ fileName: att.fileName,
8761
+ mimeType: att.mimeType,
8762
+ // Caption on first attachment only
8763
+ caption: i === 0 ? input.caption : void 0,
8764
+ replyToMessageId: i === 0 ? input.replyToMessageId : void 0,
8765
+ width: att.width,
8766
+ height: att.height,
8767
+ seconds: att.seconds,
8768
+ ptt: att.ptt
8769
+ };
8770
+ const mediaType = inferMediaType(att.fileName, att.mimeType);
8771
+ let result;
8772
+ switch (mediaType) {
8773
+ case "image":
8774
+ result = await this.sendE2EEImage(mediaInput);
8775
+ break;
8776
+ case "video":
8777
+ result = await this.sendE2EEVideo(mediaInput);
8778
+ break;
8779
+ case "audio":
8780
+ result = await this.sendE2EEAudio(mediaInput);
8781
+ break;
8782
+ default:
8783
+ result = await this.sendE2EEFile(mediaInput);
8784
+ break;
8785
+ }
8786
+ results.push(result);
8787
+ }
8788
+ return results;
8789
+ }
8790
+ return this.mediaService.sendFiles(this.requireApi(), {
8791
+ threadId: input.threadId,
8792
+ attachments: input.attachments.map((a) => ({ data: a.data, fileName: a.fileName })),
8793
+ caption: input.caption,
8794
+ replyToMessageId: input.replyToMessageId
8795
+ });
8796
+ }
8677
8797
  async sendSticker(input) {
8678
8798
  return this.mediaService.sendSticker(this.requireApi(), input);
8679
8799
  }
@@ -8713,7 +8833,6 @@ var ClientController = class {
8713
8833
  async createPoll(input) {
8714
8834
  await this.threadService.createPoll(this.requireApi(), input);
8715
8835
  }
8716
- // editMessage is now handled by the E2EE-aware method above (routes E2EE → sendE2EEEdit, else → threadService.editMessage).
8717
8836
  async addGroupMember(input) {
8718
8837
  await this.threadService.addGroupMember(this.requireApi(), input);
8719
8838
  }
@@ -8731,6 +8850,17 @@ var ClientController = class {
8731
8850
  return this.api;
8732
8851
  }
8733
8852
  };
8853
+ function inferMediaType(fileName, mimeType) {
8854
+ const mime = (mimeType ?? "").toLowerCase();
8855
+ if (mime.startsWith("image/")) return "image";
8856
+ if (mime.startsWith("video/")) return "video";
8857
+ if (mime.startsWith("audio/")) return "audio";
8858
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
8859
+ if (["jpg", "jpeg", "png", "gif", "webp", "heic", "heif", "bmp"].includes(ext)) return "image";
8860
+ if (["mp4", "mov", "avi", "mkv", "webm", "3gp", "m4v"].includes(ext)) return "video";
8861
+ if (["mp3", "ogg", "aac", "flac", "wav", "m4a", "opus"].includes(ext)) return "audio";
8862
+ return "document";
8863
+ }
8734
8864
 
8735
8865
  // src/repositories/session.repository.ts
8736
8866
  init_cjs_shims();
@@ -8992,6 +9122,17 @@ var FBClient = class {
8992
9122
  async sendFile(input) {
8993
9123
  return this.controller.sendFile(input);
8994
9124
  }
9125
+ /**
9126
+ * Send multiple files/attachments in one call.
9127
+ *
9128
+ * - **Non-E2EE threads**: All attachments land in a single Messenger message.
9129
+ * - **E2EE threads**: Each attachment is sent as a separate encrypted message
9130
+ * (E2EE does not support multi-attachment in one frame). Returns an array
9131
+ * with one result per attachment.
9132
+ */
9133
+ async sendFiles(input) {
9134
+ return this.controller.sendFiles(input);
9135
+ }
8995
9136
  };
8996
9137
  // Annotate the CommonJS export names for ESM import in node:
8997
9138
  0 && (module.exports = {