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/dist/index.d.cts
CHANGED
|
@@ -245,6 +245,37 @@ interface SendMediaInput {
|
|
|
245
245
|
/** Whether E2EE audio should be sent as push-to-talk/voice. Defaults to true for sendAudio. */
|
|
246
246
|
ptt?: boolean;
|
|
247
247
|
}
|
|
248
|
+
/** A single attachment item inside a multi-attachment message. */
|
|
249
|
+
interface SendAttachmentItem {
|
|
250
|
+
data: Buffer;
|
|
251
|
+
fileName: string;
|
|
252
|
+
/** Optional MIME type; inferred from fileName extension when omitted. */
|
|
253
|
+
mimeType?: string;
|
|
254
|
+
/** Optional width/height for image or video payloads. */
|
|
255
|
+
width?: number;
|
|
256
|
+
height?: number;
|
|
257
|
+
/** Optional duration in seconds for video/audio payloads. */
|
|
258
|
+
seconds?: number;
|
|
259
|
+
/** Whether audio should be sent as push-to-talk/voice. */
|
|
260
|
+
ptt?: boolean;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Send multiple attachments in a single Messenger message.
|
|
264
|
+
*
|
|
265
|
+
* - **Non-E2EE threads**: All attachments are bundled into one FCA message,
|
|
266
|
+
* delivered as a multi-attachment post.
|
|
267
|
+
* - **E2EE threads**: Each attachment is sent as a separate encrypted E2EE
|
|
268
|
+
* media message (the protocol does not support multi-attachment in one
|
|
269
|
+
* encrypted frame), so `results` will contain one entry per attachment.
|
|
270
|
+
*/
|
|
271
|
+
interface SendMultipleMediaInput {
|
|
272
|
+
threadId: string;
|
|
273
|
+
/** Array of file attachments to send. At least one entry is required. */
|
|
274
|
+
attachments: SendAttachmentItem[];
|
|
275
|
+
/** Optional caption applied to the first attachment (or to the bundled non-E2EE message). */
|
|
276
|
+
caption?: string;
|
|
277
|
+
replyToMessageId?: string;
|
|
278
|
+
}
|
|
248
279
|
interface TypingInput {
|
|
249
280
|
threadId: string;
|
|
250
281
|
isTyping: boolean;
|
|
@@ -259,6 +290,30 @@ interface SendReactionInput {
|
|
|
259
290
|
/** Alias for senderJid for callers that prefer explicit target naming. */
|
|
260
291
|
targetSenderJid?: string;
|
|
261
292
|
}
|
|
293
|
+
interface UnsendMessageInput {
|
|
294
|
+
/** The message ID to revoke/unsend. */
|
|
295
|
+
messageId: string;
|
|
296
|
+
/**
|
|
297
|
+
* The thread ID (or E2EE chat JID) that contains the message.
|
|
298
|
+
* Required to route the revoke through the correct channel:
|
|
299
|
+
* - E2EE threads → encrypted revoke via Noise socket.
|
|
300
|
+
* - Non-E2EE threads → HTTP unsend via fca-unofficial.
|
|
301
|
+
*/
|
|
302
|
+
threadId: string;
|
|
303
|
+
/**
|
|
304
|
+
* Whether the message was sent by the current user.
|
|
305
|
+
* Defaults to true. Set to false for admin revoke of others' messages.
|
|
306
|
+
*/
|
|
307
|
+
fromMe?: boolean;
|
|
308
|
+
}
|
|
309
|
+
interface E2EEEditMessageInput {
|
|
310
|
+
/** The thread ID (or E2EE chat JID) containing the message. */
|
|
311
|
+
threadId: string;
|
|
312
|
+
/** The ID of the message to edit. */
|
|
313
|
+
messageId: string;
|
|
314
|
+
/** The new text content. */
|
|
315
|
+
newText: string;
|
|
316
|
+
}
|
|
262
317
|
|
|
263
318
|
interface ClientOptions {
|
|
264
319
|
appStatePath?: string;
|
|
@@ -314,12 +369,22 @@ declare class FBClient {
|
|
|
314
369
|
sendNoiseKeepAlive(): Promise<void>;
|
|
315
370
|
sendMessage(input: SendMessageInput): Promise<Record<string, unknown>>;
|
|
316
371
|
sendReaction(input: SendReactionInput): Promise<void>;
|
|
317
|
-
unsendMessage(
|
|
372
|
+
unsendMessage(input: UnsendMessageInput): Promise<void>;
|
|
373
|
+
editMessage(input: E2EEEditMessageInput): Promise<void>;
|
|
318
374
|
sendTyping(input: TypingInput): Promise<void>;
|
|
319
375
|
sendImage(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
320
376
|
sendVideo(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
321
377
|
sendAudio(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
322
378
|
sendFile(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
379
|
+
/**
|
|
380
|
+
* Send multiple files/attachments in one call.
|
|
381
|
+
*
|
|
382
|
+
* - **Non-E2EE threads**: All attachments land in a single Messenger message.
|
|
383
|
+
* - **E2EE threads**: Each attachment is sent as a separate encrypted message
|
|
384
|
+
* (E2EE does not support multi-attachment in one frame). Returns an array
|
|
385
|
+
* with one result per attachment.
|
|
386
|
+
*/
|
|
387
|
+
sendFiles(input: SendMultipleMediaInput): Promise<Record<string, unknown> | Record<string, unknown>[]>;
|
|
323
388
|
}
|
|
324
389
|
|
|
325
390
|
interface EncryptMediaResult {
|
|
@@ -882,4 +947,4 @@ interface AuthConfig {
|
|
|
882
947
|
platform: Platform;
|
|
883
948
|
}
|
|
884
949
|
|
|
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 };
|
|
950
|
+
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 SendAttachmentItem, type SendMediaInput, type SendMessageInput, type SendMultipleMediaInput, type SendReactionInput, type SessionData, type TypingInput, type UnsendMessageInput };
|
package/dist/index.d.ts
CHANGED
|
@@ -245,6 +245,37 @@ interface SendMediaInput {
|
|
|
245
245
|
/** Whether E2EE audio should be sent as push-to-talk/voice. Defaults to true for sendAudio. */
|
|
246
246
|
ptt?: boolean;
|
|
247
247
|
}
|
|
248
|
+
/** A single attachment item inside a multi-attachment message. */
|
|
249
|
+
interface SendAttachmentItem {
|
|
250
|
+
data: Buffer;
|
|
251
|
+
fileName: string;
|
|
252
|
+
/** Optional MIME type; inferred from fileName extension when omitted. */
|
|
253
|
+
mimeType?: string;
|
|
254
|
+
/** Optional width/height for image or video payloads. */
|
|
255
|
+
width?: number;
|
|
256
|
+
height?: number;
|
|
257
|
+
/** Optional duration in seconds for video/audio payloads. */
|
|
258
|
+
seconds?: number;
|
|
259
|
+
/** Whether audio should be sent as push-to-talk/voice. */
|
|
260
|
+
ptt?: boolean;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Send multiple attachments in a single Messenger message.
|
|
264
|
+
*
|
|
265
|
+
* - **Non-E2EE threads**: All attachments are bundled into one FCA message,
|
|
266
|
+
* delivered as a multi-attachment post.
|
|
267
|
+
* - **E2EE threads**: Each attachment is sent as a separate encrypted E2EE
|
|
268
|
+
* media message (the protocol does not support multi-attachment in one
|
|
269
|
+
* encrypted frame), so `results` will contain one entry per attachment.
|
|
270
|
+
*/
|
|
271
|
+
interface SendMultipleMediaInput {
|
|
272
|
+
threadId: string;
|
|
273
|
+
/** Array of file attachments to send. At least one entry is required. */
|
|
274
|
+
attachments: SendAttachmentItem[];
|
|
275
|
+
/** Optional caption applied to the first attachment (or to the bundled non-E2EE message). */
|
|
276
|
+
caption?: string;
|
|
277
|
+
replyToMessageId?: string;
|
|
278
|
+
}
|
|
248
279
|
interface TypingInput {
|
|
249
280
|
threadId: string;
|
|
250
281
|
isTyping: boolean;
|
|
@@ -259,6 +290,30 @@ interface SendReactionInput {
|
|
|
259
290
|
/** Alias for senderJid for callers that prefer explicit target naming. */
|
|
260
291
|
targetSenderJid?: string;
|
|
261
292
|
}
|
|
293
|
+
interface UnsendMessageInput {
|
|
294
|
+
/** The message ID to revoke/unsend. */
|
|
295
|
+
messageId: string;
|
|
296
|
+
/**
|
|
297
|
+
* The thread ID (or E2EE chat JID) that contains the message.
|
|
298
|
+
* Required to route the revoke through the correct channel:
|
|
299
|
+
* - E2EE threads → encrypted revoke via Noise socket.
|
|
300
|
+
* - Non-E2EE threads → HTTP unsend via fca-unofficial.
|
|
301
|
+
*/
|
|
302
|
+
threadId: string;
|
|
303
|
+
/**
|
|
304
|
+
* Whether the message was sent by the current user.
|
|
305
|
+
* Defaults to true. Set to false for admin revoke of others' messages.
|
|
306
|
+
*/
|
|
307
|
+
fromMe?: boolean;
|
|
308
|
+
}
|
|
309
|
+
interface E2EEEditMessageInput {
|
|
310
|
+
/** The thread ID (or E2EE chat JID) containing the message. */
|
|
311
|
+
threadId: string;
|
|
312
|
+
/** The ID of the message to edit. */
|
|
313
|
+
messageId: string;
|
|
314
|
+
/** The new text content. */
|
|
315
|
+
newText: string;
|
|
316
|
+
}
|
|
262
317
|
|
|
263
318
|
interface ClientOptions {
|
|
264
319
|
appStatePath?: string;
|
|
@@ -314,12 +369,22 @@ declare class FBClient {
|
|
|
314
369
|
sendNoiseKeepAlive(): Promise<void>;
|
|
315
370
|
sendMessage(input: SendMessageInput): Promise<Record<string, unknown>>;
|
|
316
371
|
sendReaction(input: SendReactionInput): Promise<void>;
|
|
317
|
-
unsendMessage(
|
|
372
|
+
unsendMessage(input: UnsendMessageInput): Promise<void>;
|
|
373
|
+
editMessage(input: E2EEEditMessageInput): Promise<void>;
|
|
318
374
|
sendTyping(input: TypingInput): Promise<void>;
|
|
319
375
|
sendImage(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
320
376
|
sendVideo(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
321
377
|
sendAudio(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
322
378
|
sendFile(input: SendMediaInput): Promise<Record<string, unknown>>;
|
|
379
|
+
/**
|
|
380
|
+
* Send multiple files/attachments in one call.
|
|
381
|
+
*
|
|
382
|
+
* - **Non-E2EE threads**: All attachments land in a single Messenger message.
|
|
383
|
+
* - **E2EE threads**: Each attachment is sent as a separate encrypted message
|
|
384
|
+
* (E2EE does not support multi-attachment in one frame). Returns an array
|
|
385
|
+
* with one result per attachment.
|
|
386
|
+
*/
|
|
387
|
+
sendFiles(input: SendMultipleMediaInput): Promise<Record<string, unknown> | Record<string, unknown>[]>;
|
|
323
388
|
}
|
|
324
389
|
|
|
325
390
|
interface EncryptMediaResult {
|
|
@@ -882,4 +947,4 @@ interface AuthConfig {
|
|
|
882
947
|
platform: Platform;
|
|
883
948
|
}
|
|
884
949
|
|
|
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 };
|
|
950
|
+
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 SendAttachmentItem, type SendMediaInput, type SendMessageInput, type SendMultipleMediaInput, type SendReactionInput, type SessionData, type TypingInput, type UnsendMessageInput };
|
package/dist/index.js
CHANGED
|
@@ -3465,6 +3465,30 @@ var FacebookGatewayService = class {
|
|
|
3465
3465
|
);
|
|
3466
3466
|
return response ?? {};
|
|
3467
3467
|
}
|
|
3468
|
+
/**
|
|
3469
|
+
* Send multiple attachments in a single FCA message.
|
|
3470
|
+
* FCA-unofficial accepts an array in `attachment` field, which bundles all
|
|
3471
|
+
* files into one Messenger message on the wire.
|
|
3472
|
+
*/
|
|
3473
|
+
async sendMultipleAttachmentsMessage(api, input) {
|
|
3474
|
+
if (input.attachments.length === 0) {
|
|
3475
|
+
throw new Error("sendMultipleAttachmentsMessage requires at least one attachment");
|
|
3476
|
+
}
|
|
3477
|
+
const streams = input.attachments.map(({ data, fileName }) => {
|
|
3478
|
+
const stream = Readable.from(data);
|
|
3479
|
+
Object.assign(stream, { path: fileName });
|
|
3480
|
+
return stream;
|
|
3481
|
+
});
|
|
3482
|
+
const payload = {
|
|
3483
|
+
body: input.caption ?? "",
|
|
3484
|
+
// FCA-unofficial accepts a single Readable or an array of Readables
|
|
3485
|
+
attachment: streams.length === 1 ? streams[0] : streams
|
|
3486
|
+
};
|
|
3487
|
+
const response = await Promise.resolve(
|
|
3488
|
+
api.sendMessage(payload, input.threadId, void 0, input.replyToMessageId)
|
|
3489
|
+
);
|
|
3490
|
+
return response ?? {};
|
|
3491
|
+
}
|
|
3468
3492
|
async sendReaction(api, messageId, reaction) {
|
|
3469
3493
|
if (!api.setMessageReaction) {
|
|
3470
3494
|
throw new Error("setMessageReaction is not available in fca-unofficial");
|
|
@@ -3641,6 +3665,13 @@ var MediaService = class {
|
|
|
3641
3665
|
replyToMessageId: input.replyToMessageId
|
|
3642
3666
|
});
|
|
3643
3667
|
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Send multiple attachments bundled into a single FCA message.
|
|
3670
|
+
* Non-E2EE only — for E2EE threads use the controller which sends them sequentially.
|
|
3671
|
+
*/
|
|
3672
|
+
async sendFiles(api, input) {
|
|
3673
|
+
return this.gateway.sendMultipleAttachmentsMessage(api, input);
|
|
3674
|
+
}
|
|
3644
3675
|
async sendSticker(api, input) {
|
|
3645
3676
|
return this.gateway.sendStickerMessage(api, {
|
|
3646
3677
|
threadId: input.threadId,
|
|
@@ -8208,12 +8239,13 @@ var ClientController = class {
|
|
|
8208
8239
|
const isE2EE = this.isE2EEThreadId(input.threadId);
|
|
8209
8240
|
const isGroup = input.threadId.includes("@g.us") || input.threadId.includes(".g.");
|
|
8210
8241
|
if (this.e2eeConnected && isE2EE) {
|
|
8242
|
+
let messageId;
|
|
8211
8243
|
if (isGroup) {
|
|
8212
|
-
await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
|
|
8244
|
+
messageId = await this.sendE2EEGroupText(input.threadId, input.text, input.replyToMessageId);
|
|
8213
8245
|
} else {
|
|
8214
|
-
await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
|
|
8246
|
+
messageId = await this.sendE2EEText(input.threadId, input.text, input.replyToMessageId);
|
|
8215
8247
|
}
|
|
8216
|
-
return { messageId
|
|
8248
|
+
return { messageId, timestampMs: now() };
|
|
8217
8249
|
}
|
|
8218
8250
|
return this.messagingService.sendText(this.requireApi(), input);
|
|
8219
8251
|
}
|
|
@@ -8273,6 +8305,7 @@ var ClientController = class {
|
|
|
8273
8305
|
createdAtMs: now()
|
|
8274
8306
|
});
|
|
8275
8307
|
logger.info("ClientController", `E2EE DM message sent to ${toJid} with ${participantNodes.length} devices`);
|
|
8308
|
+
return messageId;
|
|
8276
8309
|
}
|
|
8277
8310
|
async sendE2EEGroupText(groupJid, text, replyToMessageId) {
|
|
8278
8311
|
if (!this.e2eeSocket) throw new Error("E2EE not connected");
|
|
@@ -8335,6 +8368,7 @@ var ClientController = class {
|
|
|
8335
8368
|
createdAtMs: now()
|
|
8336
8369
|
});
|
|
8337
8370
|
logger.info("ClientController", `E2EE Group message sent to ${groupJid} with ${participantNodes.length} devices`);
|
|
8371
|
+
return messageId;
|
|
8338
8372
|
}
|
|
8339
8373
|
getSelfE2EEJid() {
|
|
8340
8374
|
const device = this.activeDeviceStore?.jidDevice ?? 0;
|
|
@@ -8346,8 +8380,210 @@ var ClientController = class {
|
|
|
8346
8380
|
async sendReaction(input) {
|
|
8347
8381
|
await this.messagingService.react(this.requireApi(), input);
|
|
8348
8382
|
}
|
|
8349
|
-
|
|
8350
|
-
|
|
8383
|
+
/**
|
|
8384
|
+
* Unsend/revoke a message.
|
|
8385
|
+
*
|
|
8386
|
+
* - **E2EE threads**: Sends an encrypted `ConsumerApplication { applicationData { revoke } }`
|
|
8387
|
+
* message over the Noise socket. The `fromMe` flag in the revoke key determines
|
|
8388
|
+
* whether it is a sender revoke (`true`, default) or an admin revoke (`false`).
|
|
8389
|
+
* - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP unsend.
|
|
8390
|
+
*/
|
|
8391
|
+
async unsendMessage(input) {
|
|
8392
|
+
const isE2EE = this.isE2EEThreadId(input.threadId);
|
|
8393
|
+
if (this.e2eeConnected && isE2EE) {
|
|
8394
|
+
await this.sendE2EERevoke(input.threadId, input.messageId, input.fromMe ?? true);
|
|
8395
|
+
return;
|
|
8396
|
+
}
|
|
8397
|
+
await this.messagingService.unsend(this.requireApi(), input.messageId);
|
|
8398
|
+
}
|
|
8399
|
+
/**
|
|
8400
|
+
* Send an E2EE revoke (unsend) for a DM or group message.
|
|
8401
|
+
*
|
|
8402
|
+
* Builds a `ConsumerApplication { applicationData { revoke { key { id, fromMe } } } }`
|
|
8403
|
+
* payload, wraps it in MessageApplication + MessageTransport, then fans out to all
|
|
8404
|
+
* participant devices exactly like a normal DM or group text — but with the
|
|
8405
|
+
* correct edit attribute on the `<message>` node so the server and
|
|
8406
|
+
* receiving clients apply the correct revoke semantics.
|
|
8407
|
+
*/
|
|
8408
|
+
async sendE2EERevoke(threadId, messageId, fromMe) {
|
|
8409
|
+
if (!this.e2eeSocket) throw new Error("E2EE not connected");
|
|
8410
|
+
const e2eeClient = this.e2eeService.getClient();
|
|
8411
|
+
const selfJid = this.getSelfE2EEJid();
|
|
8412
|
+
const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
|
|
8413
|
+
const editAttr = fromMe ? "7" : "8";
|
|
8414
|
+
const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
|
|
8415
|
+
const consumerApp = e2eeClient.buildRevokeMessage(messageId, { fromMe, remoteJid: toJid });
|
|
8416
|
+
const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
|
|
8417
|
+
const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
|
|
8418
|
+
if (isGroup) {
|
|
8419
|
+
const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
|
|
8420
|
+
const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
|
|
8421
|
+
const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
|
|
8422
|
+
const groupResult = await e2eeClient.encryptGroupMessageApplication(
|
|
8423
|
+
threadId,
|
|
8424
|
+
selfJid,
|
|
8425
|
+
messageApp,
|
|
8426
|
+
newMessageId
|
|
8427
|
+
);
|
|
8428
|
+
const participantNodes = [];
|
|
8429
|
+
for (const deviceJid of deviceJids) {
|
|
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) ? groupResult.selfDevicePayload : groupResult.devicePayload;
|
|
8436
|
+
const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
|
|
8437
|
+
participantNodes.push(encodeNode("to", { jid: deviceJid }, [
|
|
8438
|
+
encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
|
|
8439
|
+
]));
|
|
8440
|
+
} catch (err) {
|
|
8441
|
+
logger.error("ClientController", `Failed to distribute revoke SKDM to ${deviceJid}:`, err);
|
|
8442
|
+
}
|
|
8443
|
+
}
|
|
8444
|
+
const phash = buildParticipantListHash(deviceJids);
|
|
8445
|
+
const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
|
|
8446
|
+
encodeNode("participants", {}, participantNodes),
|
|
8447
|
+
encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
|
|
8448
|
+
encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))]),
|
|
8449
|
+
encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
|
|
8450
|
+
]);
|
|
8451
|
+
await this.e2eeSocket.sendFrame(marshal(msgNode));
|
|
8452
|
+
logger.info("ClientController", `E2EE group revoke sent for message ${messageId} in ${threadId}`);
|
|
8453
|
+
} else {
|
|
8454
|
+
const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
|
|
8455
|
+
const selfDevicePayload = e2eeClient.buildMessageTransport({
|
|
8456
|
+
messageApp,
|
|
8457
|
+
dsm: { destinationJid: toJid, phash: "" }
|
|
8458
|
+
});
|
|
8459
|
+
const participantNodes = [];
|
|
8460
|
+
const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
|
|
8461
|
+
for (const deviceJid of deviceJids) {
|
|
8462
|
+
if (sameMessengerDevice(deviceJid, selfJid)) continue;
|
|
8463
|
+
try {
|
|
8464
|
+
if (!await e2eeClient.hasSession(deviceJid)) {
|
|
8465
|
+
const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
|
|
8466
|
+
await e2eeClient.establishSession(deviceJid, bundle);
|
|
8467
|
+
}
|
|
8468
|
+
const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
|
|
8469
|
+
const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
|
|
8470
|
+
participantNodes.push(encodeNode("to", { jid: deviceJid }, [
|
|
8471
|
+
encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
|
|
8472
|
+
]));
|
|
8473
|
+
} catch (err) {
|
|
8474
|
+
logger.error("ClientController", `Failed to encrypt revoke to ${deviceJid}:`, err);
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8477
|
+
const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
|
|
8478
|
+
encodeNode("participants", {}, participantNodes),
|
|
8479
|
+
encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
|
|
8480
|
+
encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))])
|
|
8481
|
+
]);
|
|
8482
|
+
await this.e2eeSocket.sendFrame(marshal(msgNode));
|
|
8483
|
+
logger.info("ClientController", `E2EE DM revoke sent for message ${messageId} to ${toJid}`);
|
|
8484
|
+
}
|
|
8485
|
+
}
|
|
8486
|
+
/**
|
|
8487
|
+
* Edit an E2EE message (change its text).
|
|
8488
|
+
*
|
|
8489
|
+
* - **E2EE threads**: Sends an encrypted `ConsumerApplication { content { editMessage { key, message, timestampMS } } }`
|
|
8490
|
+
* payload with the edit message attribute set.
|
|
8491
|
+
* - **Non-E2EE threads**: Falls back to `fca-unofficial` HTTP edit.
|
|
8492
|
+
*/
|
|
8493
|
+
async editMessage(input) {
|
|
8494
|
+
const isE2EE = this.isE2EEThreadId(input.threadId);
|
|
8495
|
+
if (this.e2eeConnected && isE2EE) {
|
|
8496
|
+
await this.sendE2EEEdit(input.threadId, input.messageId, input.newText);
|
|
8497
|
+
return;
|
|
8498
|
+
}
|
|
8499
|
+
await this.threadService.editMessage(this.requireApi(), {
|
|
8500
|
+
messageId: input.messageId,
|
|
8501
|
+
newText: input.newText
|
|
8502
|
+
});
|
|
8503
|
+
}
|
|
8504
|
+
/**
|
|
8505
|
+
* Send an E2EE message edit for a DM or group message.
|
|
8506
|
+
*
|
|
8507
|
+
* Builds a `ConsumerApplication { payload { content { editMessage { key, message, timestampMS } } } }`
|
|
8508
|
+
* payload, fanned out to all participant devices exactly like a normal message send.
|
|
8509
|
+
*/
|
|
8510
|
+
async sendE2EEEdit(threadId, messageId, newText) {
|
|
8511
|
+
if (!this.e2eeSocket) throw new Error("E2EE not connected");
|
|
8512
|
+
const e2eeClient = this.e2eeService.getClient();
|
|
8513
|
+
const selfJid = this.getSelfE2EEJid();
|
|
8514
|
+
const isGroup = threadId.includes("@g.us") || threadId.includes(".g.");
|
|
8515
|
+
const editAttr = "1";
|
|
8516
|
+
const toJid = isGroup ? threadId : normalizeDMThreadToJid(threadId);
|
|
8517
|
+
const consumerApp = e2eeClient.buildEditMessage(messageId, newText);
|
|
8518
|
+
const { messageApp, frankingTag } = e2eeClient.buildMessageApplication(consumerApp);
|
|
8519
|
+
const newMessageId = String(BigInt(Math.floor(Math.random() * 1e15)));
|
|
8520
|
+
if (isGroup) {
|
|
8521
|
+
const memberJids = await this.e2eeHandler.getGroupParticipants(threadId);
|
|
8522
|
+
const deviceUsers = uniqueJids([...memberJids, toBareMessengerJid(selfJid)]);
|
|
8523
|
+
const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList(deviceUsers)).filter((jid) => !sameMessengerDevice(jid, selfJid));
|
|
8524
|
+
const groupResult = await e2eeClient.encryptGroupMessageApplication(
|
|
8525
|
+
threadId,
|
|
8526
|
+
selfJid,
|
|
8527
|
+
messageApp,
|
|
8528
|
+
newMessageId
|
|
8529
|
+
);
|
|
8530
|
+
const participantNodes = [];
|
|
8531
|
+
for (const deviceJid of deviceJids) {
|
|
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) ? groupResult.selfDevicePayload : groupResult.devicePayload;
|
|
8538
|
+
const skdmEnc = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
|
|
8539
|
+
participantNodes.push(encodeNode("to", { jid: deviceJid }, [
|
|
8540
|
+
encodeNode("enc", { v: "3", type: skdmEnc.type }, skdmEnc.ciphertext)
|
|
8541
|
+
]));
|
|
8542
|
+
} catch (err) {
|
|
8543
|
+
logger.error("ClientController", `Failed to distribute edit SKDM to ${deviceJid}:`, err);
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
const phash = buildParticipantListHash(deviceJids);
|
|
8547
|
+
const msgNode = encodeNode("message", { to: threadId, type: "text", id: newMessageId, phash, edit: editAttr }, [
|
|
8548
|
+
encodeNode("participants", {}, participantNodes),
|
|
8549
|
+
encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
|
|
8550
|
+
encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))]),
|
|
8551
|
+
encodeNode("enc", { v: "3", type: "skmsg", "decrypt-fail": "hide" }, groupResult.groupCiphertext)
|
|
8552
|
+
]);
|
|
8553
|
+
await this.e2eeSocket.sendFrame(marshal(msgNode));
|
|
8554
|
+
logger.info("ClientController", `E2EE group edit sent for message ${messageId} in ${threadId}`);
|
|
8555
|
+
} else {
|
|
8556
|
+
const devicePayload = e2eeClient.buildMessageTransport({ messageApp });
|
|
8557
|
+
const selfDevicePayload = e2eeClient.buildMessageTransport({
|
|
8558
|
+
messageApp,
|
|
8559
|
+
dsm: { destinationJid: toJid, phash: "" }
|
|
8560
|
+
});
|
|
8561
|
+
const participantNodes = [];
|
|
8562
|
+
const deviceJids = uniqueJids(await this.e2eeHandler.getDeviceList([toJid, toBareMessengerJid(selfJid)]));
|
|
8563
|
+
for (const deviceJid of deviceJids) {
|
|
8564
|
+
if (sameMessengerDevice(deviceJid, selfJid)) continue;
|
|
8565
|
+
try {
|
|
8566
|
+
if (!await e2eeClient.hasSession(deviceJid)) {
|
|
8567
|
+
const bundle = await this.e2eeHandler.getPreKeyBundle(deviceJid);
|
|
8568
|
+
await e2eeClient.establishSession(deviceJid, bundle);
|
|
8569
|
+
}
|
|
8570
|
+
const payload = sameMessengerUser(deviceJid, selfJid) ? selfDevicePayload : devicePayload;
|
|
8571
|
+
const encrypted = await e2eeClient.encryptDevicePayload(deviceJid, selfJid, payload);
|
|
8572
|
+
participantNodes.push(encodeNode("to", { jid: deviceJid }, [
|
|
8573
|
+
encodeNode("enc", { v: "3", type: encrypted.type, "decrypt-fail": "hide" }, encrypted.ciphertext)
|
|
8574
|
+
]));
|
|
8575
|
+
} catch (err) {
|
|
8576
|
+
logger.error("ClientController", `Failed to encrypt edit to ${deviceJid}:`, err);
|
|
8577
|
+
}
|
|
8578
|
+
}
|
|
8579
|
+
const msgNode = encodeNode("message", { to: toJid, type: "text", id: newMessageId, edit: editAttr }, [
|
|
8580
|
+
encodeNode("participants", {}, participantNodes),
|
|
8581
|
+
encodeNode("franking", {}, [encodeNode("franking_tag", {}, frankingTag)]),
|
|
8582
|
+
encodeNode("trace", {}, [encodeNode("request_id", {}, Buffer.from(randomUUID3().replace(/-/g, ""), "hex"))])
|
|
8583
|
+
]);
|
|
8584
|
+
await this.e2eeSocket.sendFrame(marshal(msgNode));
|
|
8585
|
+
logger.info("ClientController", `E2EE DM edit sent for message ${messageId} to ${toJid}`);
|
|
8586
|
+
}
|
|
8351
8587
|
}
|
|
8352
8588
|
async sendTyping(input) {
|
|
8353
8589
|
await this.messagingService.sendTyping(this.requireApi(), input);
|
|
@@ -8355,7 +8591,7 @@ var ClientController = class {
|
|
|
8355
8591
|
async markAsRead(input) {
|
|
8356
8592
|
await this.messagingService.markAsRead(this.requireApi(), input);
|
|
8357
8593
|
}
|
|
8358
|
-
//
|
|
8594
|
+
// E2EE Media Upload Config
|
|
8359
8595
|
async getE2EEMediaUploadConfig() {
|
|
8360
8596
|
if (this.e2eeUploadConfig && !this.isMediaUploadConfigExpired(this.e2eeUploadConfig)) {
|
|
8361
8597
|
return this.e2eeUploadConfig;
|
|
@@ -8502,6 +8738,62 @@ var ClientController = class {
|
|
|
8502
8738
|
}
|
|
8503
8739
|
return this.mediaService.sendFile(this.requireApi(), input);
|
|
8504
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
|
+
}
|
|
8505
8797
|
async sendSticker(input) {
|
|
8506
8798
|
return this.mediaService.sendSticker(this.requireApi(), input);
|
|
8507
8799
|
}
|
|
@@ -8541,9 +8833,6 @@ var ClientController = class {
|
|
|
8541
8833
|
async createPoll(input) {
|
|
8542
8834
|
await this.threadService.createPoll(this.requireApi(), input);
|
|
8543
8835
|
}
|
|
8544
|
-
async editMessage(input) {
|
|
8545
|
-
return this.threadService.editMessage(this.requireApi(), input);
|
|
8546
|
-
}
|
|
8547
8836
|
async addGroupMember(input) {
|
|
8548
8837
|
await this.threadService.addGroupMember(this.requireApi(), input);
|
|
8549
8838
|
}
|
|
@@ -8561,6 +8850,17 @@ var ClientController = class {
|
|
|
8561
8850
|
return this.api;
|
|
8562
8851
|
}
|
|
8563
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
|
+
}
|
|
8564
8864
|
|
|
8565
8865
|
// src/repositories/session.repository.ts
|
|
8566
8866
|
init_esm_shims();
|
|
@@ -8800,8 +9100,11 @@ var FBClient = class {
|
|
|
8800
9100
|
async sendReaction(input) {
|
|
8801
9101
|
await this.controller.sendReaction(input);
|
|
8802
9102
|
}
|
|
8803
|
-
async unsendMessage(
|
|
8804
|
-
await this.controller.unsendMessage(
|
|
9103
|
+
async unsendMessage(input) {
|
|
9104
|
+
await this.controller.unsendMessage(input);
|
|
9105
|
+
}
|
|
9106
|
+
async editMessage(input) {
|
|
9107
|
+
await this.controller.editMessage(input);
|
|
8805
9108
|
}
|
|
8806
9109
|
async sendTyping(input) {
|
|
8807
9110
|
await this.controller.sendTyping(input);
|
|
@@ -8819,6 +9122,17 @@ var FBClient = class {
|
|
|
8819
9122
|
async sendFile(input) {
|
|
8820
9123
|
return this.controller.sendFile(input);
|
|
8821
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
|
+
}
|
|
8822
9136
|
};
|
|
8823
9137
|
export {
|
|
8824
9138
|
E2EEService,
|