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 +82 -3
- package/dist/index.cjs +325 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -2
- package/dist/index.d.ts +67 -2
- package/dist/index.js +325 -11
- 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.
|
|
@@ -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
|
|
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
|
-
|
|
8317
|
-
|
|
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
|
-
//
|
|
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(
|
|
8771
|
-
await this.controller.unsendMessage(
|
|
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 = {
|