fb-messenger-e2ee 0.1.2 → 0.1.3

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.
package/dist/index.cjs CHANGED
@@ -8175,12 +8175,13 @@ var ClientController = class {
8175
8175
  const isE2EE = this.isE2EEThreadId(input.threadId);
8176
8176
  const isGroup = input.threadId.includes("@g.us") || input.threadId.includes(".g.");
8177
8177
  if (this.e2eeConnected && isE2EE) {
8178
+ let messageId;
8178
8179
  if (isGroup) {
8179
- await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8180
+ messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8180
8181
  } else {
8181
- await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8182
+ messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8182
8183
  }
8183
- return { messageId: `e2ee-${now()}`, timestampMs: now() };
8184
+ return { messageId, timestampMs: now() };
8184
8185
  }
8185
8186
  return this.messagingService.sendText(this.requireApi(), input);
8186
8187
  }
@@ -8240,6 +8241,7 @@ var ClientController = class {
8240
8241
  createdAtMs: now()
8241
8242
  });
8242
8243
  logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
8244
+ return messageId;
8243
8245
  }
8244
8246
  async sendE2EEGroupText(groupJid, text, replyToMessageId) {
8245
8247
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
@@ -8302,6 +8304,7 @@ var ClientController = class {
8302
8304
  createdAtMs: now()
8303
8305
  });
8304
8306
  logger.info("ClientController", `E2EE Group message sent to ${groupJid} with ${participantNodes.length} devices`);
8307
+ return messageId;
8305
8308
  }
8306
8309
  getSelfE2EEJid() {
8307
8310
  const device = this.activeDeviceStore?.jidDevice ?? 0;
@@ -8313,8 +8316,210 @@ var ClientController = class {
8313
8316
  async sendReaction(input) {
8314
8317
  await this.messagingService.react(this.requireApi(), input);
8315
8318
  }
8316
- async unsendMessage(messageId) {
8317
- await this.messagingService.unsend(this.requireApi(), messageId);
8319
+ /**
8320
+ * Unsend/revoke a message.
8321
+ *
8322
+ * - **E2EE threads**: Sends an encrypted `ConsumerApplication { applicationData { revoke } }`
8323
+ * message over the Noise socket. The `fromMe` flag in the revoke key determines
8324
+ * whether it is a sender revoke (`true`, default) or an admin revoke (`false`).
8325
+ * - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP unsend.
8326
+ */
8327
+ async unsendMessage(input) {
8328
+ const isE2EE = this.isE2EEThreadId(input.threadId);
8329
+ if (this.e2eeConnected && isE2EE) {
8330
+ await this.sendE2EERevoke(input.threadId, input.messageId, input.fromMe ?? true);
8331
+ return;
8332
+ }
8333
+ await this.messagingService.unsend(this.requireApi(), input.messageId);
8334
+ }
8335
+ /**
8336
+ * Send an E2EE revoke (unsend) for a DM or group message.
8337
+ *
8338
+ * Builds a `ConsumerApplication { applicationData { revoke { key { id, fromMe } } } }`
8339
+ * payload, wraps it in MessageApplication + MessageTransport, then fans out to all
8340
+ * participant devices exactly like a normal DM or group text — but with the
8341
+ * correct edit attribute on the `<message>` node so the server and
8342
+ * receiving clients apply the correct revoke semantics.
8343
+ */
8344
+ async sendE2EERevoke(threadId, messageId, fromMe) {
8345
+ if (!this.e2eeSocket) throw new Error("E2EE not connected");
8346
+ const e2eeClient = this.e2eeService.getClient();
8347
+ const selfJid = this.getSelfE2EEJid();
8348
+ const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
8349
+ const editAttr = fromMe ? "7" : "8";
8350
+ const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
8351
+ const consumerApp = e2eeClient.buildRevokeMessage(messageId, { fromMe, remoteJid: toJid });
8352
+ const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
8353
+ const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
8354
+ if (isGroup) {
8355
+ const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
8356
+ const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
8357
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
8358
+ const groupResult = await e2eeClient.encryptGroupMessageApplication(
8359
+ threadId,
8360
+ selfJid,
8361
+ messageApp,
8362
+ newMessageId
8363
+ );
8364
+ const participantNodes = [];
8365
+ for (const deviceJid of deviceJids) {
8366
+ try {
8367
+ if (!await e2eeClient.hasSession(deviceJid)) {
8368
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8369
+ await e2eeClient.establishSession(deviceJid, bundle);
8370
+ }
8371
+ const payload = sameMessengerUser(deviceJid, selfJid) ? groupResult.selfDevicePayload : groupResult.devicePayload;
8372
+ const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8373
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8374
+ encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
8375
+ ]));
8376
+ } catch (err) {
8377
+ logger.error("ClientController", `Failed to distribute revoke SKDM to ${deviceJid}:`, err);
8378
+ }
8379
+ }
8380
+ const phash = buildParticipantListHash(deviceJids);
8381
+ const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
8382
+ encodeNode("participants", {}, participantNodes),
8383
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8384
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))]),
8385
+ encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
8386
+ ]);
8387
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8388
+ logger.info("ClientController", `E2EE group revoke sent for message ${messageId} in ${threadId}`);
8389
+ } else {
8390
+ const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8391
+ const selfDevicePayload = e2eeClient.buildMessageTransport({
8392
+ messageApp,
8393
+ dsm: { destinationJid: toJid, phash: "" }
8394
+ });
8395
+ const participantNodes = [];
8396
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
8397
+ for (const deviceJid of deviceJids) {
8398
+ if (sameMessengerDevice(deviceJid, selfJid)) continue;
8399
+ try {
8400
+ if (!await e2eeClient.hasSession(deviceJid)) {
8401
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8402
+ await e2eeClient.establishSession(deviceJid, bundle);
8403
+ }
8404
+ const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
8405
+ const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8406
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8407
+ encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
8408
+ ]));
8409
+ } catch (err) {
8410
+ logger.error("ClientController", `Failed to encrypt revoke to ${deviceJid}:`, err);
8411
+ }
8412
+ }
8413
+ const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
8414
+ encodeNode("participants", {}, participantNodes),
8415
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8416
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))])
8417
+ ]);
8418
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8419
+ logger.info("ClientController", `E2EE DM revoke sent for message ${messageId} to ${toJid}`);
8420
+ }
8421
+ }
8422
+ /**
8423
+ * Edit an E2EE message (change its text).
8424
+ *
8425
+ * - **E2EE threads**: Sends an encrypted `ConsumerApplication { content { editMessage { key, message, timestampMS } } }`
8426
+ * payload with the edit message attribute set.
8427
+ * - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP edit.
8428
+ */
8429
+ async editMessage(input) {
8430
+ const isE2EE = this.isE2EEThreadId(input.threadId);
8431
+ if (this.e2eeConnected && isE2EE) {
8432
+ await this.sendE2EEEdit(input.threadId, input.messageId, input.newText);
8433
+ return;
8434
+ }
8435
+ await this.threadService.editMessage(this.requireApi(), {
8436
+ messageId: input.messageId,
8437
+ newText: input.newText
8438
+ });
8439
+ }
8440
+ /**
8441
+ * Send an E2EE message edit for a DM or group message.
8442
+ *
8443
+ * Builds a `ConsumerApplication { payload { content { editMessage { key, message, timestampMS } } } }`
8444
+ * payload, fanned out to all participant devices exactly like a normal message send.
8445
+ */
8446
+ async sendE2EEEdit(threadId, messageId, newText) {
8447
+ if (!this.e2eeSocket) throw new Error("E2EE not connected");
8448
+ const e2eeClient = this.e2eeService.getClient();
8449
+ const selfJid = this.getSelfE2EEJid();
8450
+ const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
8451
+ const editAttr = "1";
8452
+ const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
8453
+ const consumerApp = e2eeClient.buildEditMessage(messageId, newText);
8454
+ const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
8455
+ const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
8456
+ if (isGroup) {
8457
+ const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
8458
+ const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
8459
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
8460
+ const groupResult = await e2eeClient.encryptGroupMessageApplication(
8461
+ threadId,
8462
+ selfJid,
8463
+ messageApp,
8464
+ newMessageId
8465
+ );
8466
+ const participantNodes = [];
8467
+ for (const deviceJid of deviceJids) {
8468
+ try {
8469
+ if (!await e2eeClient.hasSession(deviceJid)) {
8470
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8471
+ await e2eeClient.establishSession(deviceJid, bundle);
8472
+ }
8473
+ const payload = sameMessengerUser(deviceJid, selfJid) ? groupResult.selfDevicePayload : groupResult.devicePayload;
8474
+ const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8475
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8476
+ encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
8477
+ ]));
8478
+ } catch (err) {
8479
+ logger.error("ClientController", `Failed to distribute edit SKDM to ${deviceJid}:`, err);
8480
+ }
8481
+ }
8482
+ const phash = buildParticipantListHash(deviceJids);
8483
+ const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
8484
+ encodeNode("participants", {}, participantNodes),
8485
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8486
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))]),
8487
+ encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
8488
+ ]);
8489
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8490
+ logger.info("ClientController", `E2EE group edit sent for message ${messageId} in ${threadId}`);
8491
+ } else {
8492
+ const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8493
+ const selfDevicePayload = e2eeClient.buildMessageTransport({
8494
+ messageApp,
8495
+ dsm: { destinationJid: toJid, phash: "" }
8496
+ });
8497
+ const participantNodes = [];
8498
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
8499
+ for (const deviceJid of deviceJids) {
8500
+ if (sameMessengerDevice(deviceJid, selfJid)) continue;
8501
+ try {
8502
+ if (!await e2eeClient.hasSession(deviceJid)) {
8503
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8504
+ await e2eeClient.establishSession(deviceJid, bundle);
8505
+ }
8506
+ const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
8507
+ const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8508
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8509
+ encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
8510
+ ]));
8511
+ } catch (err) {
8512
+ logger.error("ClientController", `Failed to encrypt edit to ${deviceJid}:`, err);
8513
+ }
8514
+ }
8515
+ const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
8516
+ encodeNode("participants", {}, participantNodes),
8517
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8518
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from((0, import_node_crypto11.randomUUID)().replace(/-/g, ""), "hex"))])
8519
+ ]);
8520
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8521
+ logger.info("ClientController", `E2EE DM edit sent for message ${messageId} to ${toJid}`);
8522
+ }
8318
8523
  }
8319
8524
  async sendTyping(input) {
8320
8525
  await this.messagingService.sendTyping(this.requireApi(), input);
@@ -8508,9 +8713,7 @@ var ClientController = class {
8508
8713
  async createPoll(input) {
8509
8714
  await this.threadService.createPoll(this.requireApi(), input);
8510
8715
  }
8511
- async editMessage(input) {
8512
- return this.threadService.editMessage(this.requireApi(), input);
8513
- }
8716
+ // editMessage is now handled by the E2EE-aware method above (routes E2EE → sendE2EEEdit, else → threadService.editMessage).
8514
8717
  async addGroupMember(input) {
8515
8718
  await this.threadService.addGroupMember(this.requireApi(), input);
8516
8719
  }
@@ -8767,8 +8970,11 @@ var FBClient = class {
8767
8970
  async sendReaction(input) {
8768
8971
  await this.controller.sendReaction(input);
8769
8972
  }
8770
- async unsendMessage(messageId) {
8771
- await this.controller.unsendMessage(messageId);
8973
+ async unsendMessage(input) {
8974
+ await this.controller.unsendMessage(input);
8975
+ }
8976
+ async editMessage(input) {
8977
+ await this.controller.editMessage(input);
8772
8978
  }
8773
8979
  async sendTyping(input) {
8774
8980
  await this.controller.sendTyping(input);