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 +41 -3
- package/dist/index.cjs +216 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +216 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/DOCS.md
CHANGED
|
@@ -144,14 +144,52 @@ await client.sendReaction({
|
|
|
144
144
|
});
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
### `unsendMessage(
|
|
147
|
+
### `unsendMessage(input: UnsendMessageInput)`
|
|
148
148
|
|
|
149
|
-
Un-sends/revokes an E2EE message you previously sent.
|
|
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(
|
|
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
|
|
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
|
-
|
|
8317
|
-
|
|
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
|
-
|
|
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(
|
|
8771
|
-
await this.controller.unsendMessage(
|
|
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);
|