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/dist/index.d.cts CHANGED
@@ -259,6 +259,30 @@ interface SendReactionInput {
259
259
  /** Alias for senderJid for callers that prefer explicit target naming. */
260
260
  targetSenderJid?: string;
261
261
  }
262
+ interface UnsendMessageInput {
263
+ /** The message ID to revoke/unsend. */
264
+ messageId: string;
265
+ /**
266
+ * The thread ID (or E2EE chat JID) that contains the message.
267
+ * Required to route the revoke through the correct channel:
268
+ * - E2EE threads → encrypted revoke via Noise socket.
269
+ * - Non-E2EE threads → HTTP unsend via fca-unofficial.
270
+ */
271
+ threadId: string;
272
+ /**
273
+ * Whether the message was sent by the current user.
274
+ * Defaults to true. Set to false for admin revoke of others' messages.
275
+ */
276
+ fromMe?: boolean;
277
+ }
278
+ interface E2EEEditMessageInput {
279
+ /** The thread ID (or E2EE chat JID) containing the message. */
280
+ threadId: string;
281
+ /** The ID of the message to edit. */
282
+ messageId: string;
283
+ /** The new text content. */
284
+ newText: string;
285
+ }
262
286
 
263
287
  interface ClientOptions {
264
288
  appStatePath?: string;
@@ -314,7 +338,8 @@ declare class FBClient {
314
338
  sendNoiseKeepAlive(): Promise<void>;
315
339
  sendMessage(input: SendMessageInput): Promise<Record<string, unknown>>;
316
340
  sendReaction(input: SendReactionInput): Promise<void>;
317
- unsendMessage(messageId: string): Promise<void>;
341
+ unsendMessage(input: UnsendMessageInput): Promise<void>;
342
+ editMessage(input: E2EEEditMessageInput): Promise<void>;
318
343
  sendTyping(input: TypingInput): Promise<void>;
319
344
  sendImage(input: SendMediaInput): Promise<Record<string, unknown>>;
320
345
  sendVideo(input: SendMediaInput): Promise<Record<string, unknown>>;
@@ -882,4 +907,4 @@ interface AuthConfig {
882
907
  platform: Platform;
883
908
  }
884
909
 
885
- export { type AppEnv, type Attachment, type AuthConfig, type ClientOptions, type E2EEDecryptMediaOptions, type E2EEDownloadOptions, type E2EEDownloadResult, type E2EEEncryptMediaOptions, type E2EEEncryptMediaResult, type E2EEMessage, type E2EEMessageKind, type E2EESendTextOptions, type E2EESendTextResult, E2EEService, FBClient, type MediaUploadConfig, type MediaUploadResult, type Mention, type MessengerEvent, type MessengerEventMap, type MmsTypeStr, type Platform, type ReplyTo, type SendMediaInput, type SendMessageInput, type SendReactionInput, type SessionData, type TypingInput };
910
+ export { type AppEnv, type Attachment, type AuthConfig, type ClientOptions, type E2EEDecryptMediaOptions, type E2EEDownloadOptions, type E2EEDownloadResult, type E2EEEditMessageInput, type E2EEEncryptMediaOptions, type E2EEEncryptMediaResult, type E2EEMessage, type E2EEMessageKind, type E2EESendTextOptions, type E2EESendTextResult, E2EEService, FBClient, type MediaUploadConfig, type MediaUploadResult, type Mention, type MessengerEvent, type MessengerEventMap, type MmsTypeStr, type Platform, type ReplyTo, type SendMediaInput, type SendMessageInput, type SendReactionInput, type SessionData, type TypingInput, type UnsendMessageInput };
package/dist/index.d.ts CHANGED
@@ -259,6 +259,30 @@ interface SendReactionInput {
259
259
  /** Alias for senderJid for callers that prefer explicit target naming. */
260
260
  targetSenderJid?: string;
261
261
  }
262
+ interface UnsendMessageInput {
263
+ /** The message ID to revoke/unsend. */
264
+ messageId: string;
265
+ /**
266
+ * The thread ID (or E2EE chat JID) that contains the message.
267
+ * Required to route the revoke through the correct channel:
268
+ * - E2EE threads → encrypted revoke via Noise socket.
269
+ * - Non-E2EE threads → HTTP unsend via fca-unofficial.
270
+ */
271
+ threadId: string;
272
+ /**
273
+ * Whether the message was sent by the current user.
274
+ * Defaults to true. Set to false for admin revoke of others' messages.
275
+ */
276
+ fromMe?: boolean;
277
+ }
278
+ interface E2EEEditMessageInput {
279
+ /** The thread ID (or E2EE chat JID) containing the message. */
280
+ threadId: string;
281
+ /** The ID of the message to edit. */
282
+ messageId: string;
283
+ /** The new text content. */
284
+ newText: string;
285
+ }
262
286
 
263
287
  interface ClientOptions {
264
288
  appStatePath?: string;
@@ -314,7 +338,8 @@ declare class FBClient {
314
338
  sendNoiseKeepAlive(): Promise<void>;
315
339
  sendMessage(input: SendMessageInput): Promise<Record<string, unknown>>;
316
340
  sendReaction(input: SendReactionInput): Promise<void>;
317
- unsendMessage(messageId: string): Promise<void>;
341
+ unsendMessage(input: UnsendMessageInput): Promise<void>;
342
+ editMessage(input: E2EEEditMessageInput): Promise<void>;
318
343
  sendTyping(input: TypingInput): Promise<void>;
319
344
  sendImage(input: SendMediaInput): Promise<Record<string, unknown>>;
320
345
  sendVideo(input: SendMediaInput): Promise<Record<string, unknown>>;
@@ -882,4 +907,4 @@ interface AuthConfig {
882
907
  platform: Platform;
883
908
  }
884
909
 
885
- export { type AppEnv, type Attachment, type AuthConfig, type ClientOptions, type E2EEDecryptMediaOptions, type E2EEDownloadOptions, type E2EEDownloadResult, type E2EEEncryptMediaOptions, type E2EEEncryptMediaResult, type E2EEMessage, type E2EEMessageKind, type E2EESendTextOptions, type E2EESendTextResult, E2EEService, FBClient, type MediaUploadConfig, type MediaUploadResult, type Mention, type MessengerEvent, type MessengerEventMap, type MmsTypeStr, type Platform, type ReplyTo, type SendMediaInput, type SendMessageInput, type SendReactionInput, type SessionData, type TypingInput };
910
+ export { type AppEnv, type Attachment, type AuthConfig, type ClientOptions, type E2EEDecryptMediaOptions, type E2EEDownloadOptions, type E2EEDownloadResult, type E2EEEditMessageInput, type E2EEEncryptMediaOptions, type E2EEEncryptMediaResult, type E2EEMessage, type E2EEMessageKind, type E2EESendTextOptions, type E2EESendTextResult, E2EEService, FBClient, type MediaUploadConfig, type MediaUploadResult, type Mention, type MessengerEvent, type MessengerEventMap, type MmsTypeStr, type Platform, type ReplyTo, type SendMediaInput, type SendMessageInput, type SendReactionInput, type SessionData, type TypingInput, type UnsendMessageInput };
package/dist/index.js CHANGED
@@ -8208,12 +8208,13 @@ var ClientController = class {
8208
8208
  const isE2EE = this.isE2EEThreadId(input.threadId);
8209
8209
  const isGroup = input.threadId.includes("@g.us") || input.threadId.includes(".g.");
8210
8210
  if (this.e2eeConnected && isE2EE) {
8211
+ let messageId;
8211
8212
  if (isGroup) {
8212
- await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8213
+ messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
8213
8214
  } else {
8214
- await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8215
+ messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
8215
8216
  }
8216
- return { messageId: `e2ee-${now()}`, timestampMs: now() };
8217
+ return { messageId, timestampMs: now() };
8217
8218
  }
8218
8219
  return this.messagingService.sendText(this.requireApi(), input);
8219
8220
  }
@@ -8273,6 +8274,7 @@ var ClientController = class {
8273
8274
  createdAtMs: now()
8274
8275
  });
8275
8276
  logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
8277
+ return messageId;
8276
8278
  }
8277
8279
  async sendE2EEGroupText(groupJid, text, replyToMessageId) {
8278
8280
  if (!this.e2eeSocket) throw new Error("E2EE not connected");
@@ -8335,6 +8337,7 @@ var ClientController = class {
8335
8337
  createdAtMs: now()
8336
8338
  });
8337
8339
  logger.info("ClientController", `E2EE Group message sent to ${groupJid} with ${participantNodes.length} devices`);
8340
+ return messageId;
8338
8341
  }
8339
8342
  getSelfE2EEJid() {
8340
8343
  const device = this.activeDeviceStore?.jidDevice ?? 0;
@@ -8346,8 +8349,210 @@ var ClientController = class {
8346
8349
  async sendReaction(input) {
8347
8350
  await this.messagingService.react(this.requireApi(), input);
8348
8351
  }
8349
- async unsendMessage(messageId) {
8350
- await this.messagingService.unsend(this.requireApi(), messageId);
8352
+ /**
8353
+ * Unsend/revoke a message.
8354
+ *
8355
+ * - **E2EE threads**: Sends an encrypted `ConsumerApplication { applicationData { revoke } }`
8356
+ * message over the Noise socket. The `fromMe` flag in the revoke key determines
8357
+ * whether it is a sender revoke (`true`, default) or an admin revoke (`false`).
8358
+ * - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP unsend.
8359
+ */
8360
+ async unsendMessage(input) {
8361
+ const isE2EE = this.isE2EEThreadId(input.threadId);
8362
+ if (this.e2eeConnected && isE2EE) {
8363
+ await this.sendE2EERevoke(input.threadId, input.messageId, input.fromMe ?? true);
8364
+ return;
8365
+ }
8366
+ await this.messagingService.unsend(this.requireApi(), input.messageId);
8367
+ }
8368
+ /**
8369
+ * Send an E2EE revoke (unsend) for a DM or group message.
8370
+ *
8371
+ * Builds a `ConsumerApplication { applicationData { revoke { key { id, fromMe } } } }`
8372
+ * payload, wraps it in MessageApplication + MessageTransport, then fans out to all
8373
+ * participant devices exactly like a normal DM or group text — but with the
8374
+ * correct edit attribute on the `<message>` node so the server and
8375
+ * receiving clients apply the correct revoke semantics.
8376
+ */
8377
+ async sendE2EERevoke(threadId, messageId, fromMe) {
8378
+ if (!this.e2eeSocket) throw new Error("E2EE not connected");
8379
+ const e2eeClient = this.e2eeService.getClient();
8380
+ const selfJid = this.getSelfE2EEJid();
8381
+ const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
8382
+ const editAttr = fromMe ? "7" : "8";
8383
+ const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
8384
+ const consumerApp = e2eeClient.buildRevokeMessage(messageId, { fromMe, remoteJid: toJid });
8385
+ const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
8386
+ const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
8387
+ if (isGroup) {
8388
+ const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
8389
+ const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
8390
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
8391
+ const groupResult = await e2eeClient.encryptGroupMessageApplication(
8392
+ threadId,
8393
+ selfJid,
8394
+ messageApp,
8395
+ newMessageId
8396
+ );
8397
+ const participantNodes = [];
8398
+ for (const deviceJid of deviceJids) {
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) ? groupResult.selfDevicePayload : groupResult.devicePayload;
8405
+ const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8406
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8407
+ encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
8408
+ ]));
8409
+ } catch (err) {
8410
+ logger.error("ClientController", `Failed to distribute revoke SKDM to ${deviceJid}:`, err);
8411
+ }
8412
+ }
8413
+ const phash = buildParticipantListHash(deviceJids);
8414
+ const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
8415
+ encodeNode("participants", {}, participantNodes),
8416
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8417
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))]),
8418
+ encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
8419
+ ]);
8420
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8421
+ logger.info("ClientController", `E2EE group revoke sent for message ${messageId} in ${threadId}`);
8422
+ } else {
8423
+ const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8424
+ const selfDevicePayload = e2eeClient.buildMessageTransport({
8425
+ messageApp,
8426
+ dsm: { destinationJid: toJid, phash: "" }
8427
+ });
8428
+ const participantNodes = [];
8429
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
8430
+ for (const deviceJid of deviceJids) {
8431
+ if (sameMessengerDevice(deviceJid, selfJid)) continue;
8432
+ try {
8433
+ if (!await e2eeClient.hasSession(deviceJid)) {
8434
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8435
+ await e2eeClient.establishSession(deviceJid, bundle);
8436
+ }
8437
+ const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
8438
+ const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8439
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8440
+ encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
8441
+ ]));
8442
+ } catch (err) {
8443
+ logger.error("ClientController", `Failed to encrypt revoke to ${deviceJid}:`, err);
8444
+ }
8445
+ }
8446
+ const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
8447
+ encodeNode("participants", {}, participantNodes),
8448
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8449
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))])
8450
+ ]);
8451
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8452
+ logger.info("ClientController", `E2EE DM revoke sent for message ${messageId} to ${toJid}`);
8453
+ }
8454
+ }
8455
+ /**
8456
+ * Edit an E2EE message (change its text).
8457
+ *
8458
+ * - **E2EE threads**: Sends an encrypted `ConsumerApplication { content { editMessage { key, message, timestampMS } } }`
8459
+ * payload with the edit message attribute set.
8460
+ * - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP edit.
8461
+ */
8462
+ async editMessage(input) {
8463
+ const isE2EE = this.isE2EEThreadId(input.threadId);
8464
+ if (this.e2eeConnected && isE2EE) {
8465
+ await this.sendE2EEEdit(input.threadId, input.messageId, input.newText);
8466
+ return;
8467
+ }
8468
+ await this.threadService.editMessage(this.requireApi(), {
8469
+ messageId: input.messageId,
8470
+ newText: input.newText
8471
+ });
8472
+ }
8473
+ /**
8474
+ * Send an E2EE message edit for a DM or group message.
8475
+ *
8476
+ * Builds a `ConsumerApplication { payload { content { editMessage { key, message, timestampMS } } } }`
8477
+ * payload, fanned out to all participant devices exactly like a normal message send.
8478
+ */
8479
+ async sendE2EEEdit(threadId, messageId, newText) {
8480
+ if (!this.e2eeSocket) throw new Error("E2EE not connected");
8481
+ const e2eeClient = this.e2eeService.getClient();
8482
+ const selfJid = this.getSelfE2EEJid();
8483
+ const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
8484
+ const editAttr = "1";
8485
+ const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
8486
+ const consumerApp = e2eeClient.buildEditMessage(messageId, newText);
8487
+ const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
8488
+ const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
8489
+ if (isGroup) {
8490
+ const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
8491
+ const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
8492
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
8493
+ const groupResult = await e2eeClient.encryptGroupMessageApplication(
8494
+ threadId,
8495
+ selfJid,
8496
+ messageApp,
8497
+ newMessageId
8498
+ );
8499
+ const participantNodes = [];
8500
+ for (const deviceJid of deviceJids) {
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) ? groupResult.selfDevicePayload : groupResult.devicePayload;
8507
+ const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8508
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8509
+ encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
8510
+ ]));
8511
+ } catch (err) {
8512
+ logger.error("ClientController", `Failed to distribute edit SKDM to ${deviceJid}:`, err);
8513
+ }
8514
+ }
8515
+ const phash = buildParticipantListHash(deviceJids);
8516
+ const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
8517
+ encodeNode("participants", {}, participantNodes),
8518
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8519
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))]),
8520
+ encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
8521
+ ]);
8522
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8523
+ logger.info("ClientController", `E2EE group edit sent for message ${messageId} in ${threadId}`);
8524
+ } else {
8525
+ const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
8526
+ const selfDevicePayload = e2eeClient.buildMessageTransport({
8527
+ messageApp,
8528
+ dsm: { destinationJid: toJid, phash: "" }
8529
+ });
8530
+ const participantNodes = [];
8531
+ const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
8532
+ for (const deviceJid of deviceJids) {
8533
+ if (sameMessengerDevice(deviceJid, selfJid)) continue;
8534
+ try {
8535
+ if (!await e2eeClient.hasSession(deviceJid)) {
8536
+ const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
8537
+ await e2eeClient.establishSession(deviceJid, bundle);
8538
+ }
8539
+ const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
8540
+ const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
8541
+ participantNodes.push(encodeNode("to", { jid: deviceJid }, [
8542
+ encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
8543
+ ]));
8544
+ } catch (err) {
8545
+ logger.error("ClientController", `Failed to encrypt edit to ${deviceJid}:`, err);
8546
+ }
8547
+ }
8548
+ const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
8549
+ encodeNode("participants", {}, participantNodes),
8550
+ encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
8551
+ encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))])
8552
+ ]);
8553
+ await this.e2eeSocket.sendFrame(marshal(msgNode));
8554
+ logger.info("ClientController", `E2EE DM edit sent for message ${messageId} to ${toJid}`);
8555
+ }
8351
8556
  }
8352
8557
  async sendTyping(input) {
8353
8558
  await this.messagingService.sendTyping(this.requireApi(), input);
@@ -8541,9 +8746,7 @@ var ClientController = class {
8541
8746
  async createPoll(input) {
8542
8747
  await this.threadService.createPoll(this.requireApi(), input);
8543
8748
  }
8544
- async editMessage(input) {
8545
- return this.threadService.editMessage(this.requireApi(), input);
8546
- }
8749
+ // editMessage is now handled by the E2EE-aware method above (routes E2EE → sendE2EEEdit, else → threadService.editMessage).
8547
8750
  async addGroupMember(input) {
8548
8751
  await this.threadService.addGroupMember(this.requireApi(), input);
8549
8752
  }
@@ -8800,8 +9003,11 @@ var FBClient = class {
8800
9003
  async sendReaction(input) {
8801
9004
  await this.controller.sendReaction(input);
8802
9005
  }
8803
- async unsendMessage(messageId) {
8804
- await this.controller.unsendMessage(messageId);
9006
+ async unsendMessage(input) {
9007
+ await this.controller.unsendMessage(input);
9008
+ }
9009
+ async editMessage(input) {
9010
+ await this.controller.editMessage(input);
8805
9011
  }
8806
9012
  async sendTyping(input) {
8807
9013
  await this.controller.sendTyping(input);