fb-messenger-e2ee 0.1.1 → 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/README.md +27 -2
- package/dist/index.cjs +9017 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +910 -0
- package/dist/index.d.ts +910 -0
- package/dist/index.js +9049 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -4
- package/bun.lock +0 -1014
- package/examples/basic-usage.ts +0 -52
- package/jest.config.ts +0 -18
- package/src/config/env.ts +0 -15
- package/src/controllers/client.controller.ts +0 -805
- package/src/controllers/dgw-handler.ts +0 -171
- package/src/controllers/e2ee-handler.ts +0 -832
- package/src/controllers/event-mapper.ts +0 -327
- package/src/core/client.ts +0 -151
- package/src/e2ee/application/e2ee-client.ts +0 -376
- package/src/e2ee/application/fanout-planner.ts +0 -79
- package/src/e2ee/application/outbound-message-cache.ts +0 -58
- package/src/e2ee/application/prekey-maintenance.ts +0 -55
- package/src/e2ee/application/retry-manager.ts +0 -156
- package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
- package/src/e2ee/facebook/icdc-payload.ts +0 -31
- package/src/e2ee/index.ts +0 -76
- package/src/e2ee/internal.ts +0 -27
- package/src/e2ee/media/media-crypto.ts +0 -147
- package/src/e2ee/media/media-upload.ts +0 -90
- package/src/e2ee/message/builders/client-payload.ts +0 -54
- package/src/e2ee/message/builders/consumer-application.ts +0 -362
- package/src/e2ee/message/builders/message-application.ts +0 -64
- package/src/e2ee/message/builders/message-transport.ts +0 -84
- package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
- package/src/e2ee/message/constants.ts +0 -4
- package/src/e2ee/message/message-builder.ts +0 -15
- package/src/e2ee/signal/prekey-manager.ts +0 -140
- package/src/e2ee/signal/signal-manager.ts +0 -365
- package/src/e2ee/store/device-json.ts +0 -47
- package/src/e2ee/store/device-repository.ts +0 -12
- package/src/e2ee/store/device-store.ts +0 -538
- package/src/e2ee/transport/binary/decoder.ts +0 -180
- package/src/e2ee/transport/binary/encoder.ts +0 -143
- package/src/e2ee/transport/binary/stanzas.ts +0 -97
- package/src/e2ee/transport/binary/tokens.ts +0 -64
- package/src/e2ee/transport/binary/wa-binary.ts +0 -8
- package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
- package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
- package/src/e2ee/transport/noise/noise-socket.ts +0 -230
- package/src/index.ts +0 -34
- package/src/models/client.ts +0 -26
- package/src/models/config.ts +0 -14
- package/src/models/domain.ts +0 -299
- package/src/models/e2ee.ts +0 -295
- package/src/models/media.ts +0 -41
- package/src/models/messaging.ts +0 -88
- package/src/models/thread.ts +0 -69
- package/src/repositories/session.repository.ts +0 -20
- package/src/services/auth.service.ts +0 -55
- package/src/services/e2ee.service.ts +0 -174
- package/src/services/facebook-gateway.service.ts +0 -245
- package/src/services/icdc.service.ts +0 -177
- package/src/services/media.service.ts +0 -304
- package/src/services/messaging.service.ts +0 -28
- package/src/services/thread.service.ts +0 -199
- package/src/types/advanced-types.ts +0 -61
- package/src/types/fca-unofficial.d.ts +0 -212
- package/src/types/protocol-types.ts +0 -43
- package/src/utils/fca-utils.ts +0 -30
- package/src/utils/logger.ts +0 -24
- package/src/utils/mime.ts +0 -51
- package/tests/.env.example +0 -87
- package/tests/data/1x1.png +0 -0
- package/tests/data/example.txt +0 -1
- package/tests/data/file_example.mp3 +0 -0
- package/tests/data/file_example.mp4 +0 -0
- package/tests/integration.test.ts +0 -498
- package/tests/script/echo-e2ee.ts +0 -174
- package/tests/script/send-e2ee-media.ts +0 -227
- package/tests/script/send-e2ee-reaction.ts +0 -108
- package/tests/script/send-e2ee-unsend.ts +0 -115
- package/tests/script/send-typing.ts +0 -107
- package/tests/script/test-group-send.ts +0 -105
- package/tests/setup.ts +0 -3
- package/tests/types.test.ts +0 -57
- package/tests/unit/controllers/dgw-handler.test.ts +0 -60
- package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
- package/tests/unit/controllers/event-mapper.test.ts +0 -252
- package/tests/unit/e2ee/application-helpers.test.ts +0 -418
- package/tests/unit/e2ee/device-store.test.ts +0 -126
- package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
- package/tests/unit/e2ee/media-crypto.test.ts +0 -55
- package/tests/unit/e2ee/media-upload.test.ts +0 -60
- package/tests/unit/e2ee/message-builder.test.ts +0 -42
- package/tests/unit/e2ee/message-builders.test.ts +0 -230
- package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
- package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
- package/tests/unit/e2ee/wa-binary.test.ts +0 -127
- package/tests/unit/services/e2ee.service.test.ts +0 -101
- package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
- package/tests/unit/services/media.service.test.ts +0 -169
- package/tests/unit/utils/fca-utils.test.ts +0 -48
- package/tsconfig.json +0 -19
- /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
package/tests/types.test.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { type AssertEqual, TypedEventEmitter } from "../src/types/advanced-types.ts";
|
|
3
|
-
import type { Attachment, ImageAttachment, VideoAttachment } from "../src/models/domain.ts";
|
|
4
|
-
import type { MessengerEventMap } from "../src/models/client.ts";
|
|
5
|
-
|
|
6
|
-
describe("Type-level Tests", () => {
|
|
7
|
-
it("AssertEqual utility works", () => {
|
|
8
|
-
type Test1 = AssertEqual<string, string>;
|
|
9
|
-
const val1: Test1 = true;
|
|
10
|
-
expect(val1).toBe(true);
|
|
11
|
-
|
|
12
|
-
type Test2 = AssertEqual<string, number>;
|
|
13
|
-
const val2: Test2 = false;
|
|
14
|
-
expect(val2).toBe(false);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("Attachment is a Discriminated Union", () => {
|
|
18
|
-
// This test is mostly for compile-time validation
|
|
19
|
-
const check = (att: Attachment) => {
|
|
20
|
-
if (att.type === "image") {
|
|
21
|
-
type IsImage = AssertEqual<typeof att, ImageAttachment>;
|
|
22
|
-
// If this compiles, it means type narrowing works
|
|
23
|
-
} else if (att.type === "video") {
|
|
24
|
-
type IsVideo = AssertEqual<typeof att, VideoAttachment>;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
expect(check).toBeDefined();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("TypedEventEmitter enforces payload types", () => {
|
|
31
|
-
type Events = {
|
|
32
|
-
ping: { count: number };
|
|
33
|
-
pong: { message: string };
|
|
34
|
-
};
|
|
35
|
-
const emitter = new TypedEventEmitter<Events>();
|
|
36
|
-
|
|
37
|
-
let receivedCount = 0;
|
|
38
|
-
emitter.on("ping", (data) => {
|
|
39
|
-
receivedCount = data.count;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
emitter.emit("ping", { count: 42 });
|
|
43
|
-
expect(receivedCount).toBe(42);
|
|
44
|
-
|
|
45
|
-
// emitter.emit("ping", { message: "error" }); // Should cause compile error
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("MessengerEventMap is correctly derived", () => {
|
|
49
|
-
type HasMessage = "message" extends keyof MessengerEventMap ? true : false;
|
|
50
|
-
const hasMessage: HasMessage = true;
|
|
51
|
-
expect(hasMessage).toBe(true);
|
|
52
|
-
|
|
53
|
-
type MessagePayload = MessengerEventMap["message"];
|
|
54
|
-
// Should be MessengerMessage
|
|
55
|
-
expect(true).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
|
2
|
-
import { DGWHandler } from "../../../src/controllers/dgw-handler.ts";
|
|
3
|
-
import { EventMapper } from "../../../src/controllers/event-mapper.ts";
|
|
4
|
-
|
|
5
|
-
describe("DGWHandler", () => {
|
|
6
|
-
let eventMapper: EventMapper;
|
|
7
|
-
let handler: DGWHandler;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
eventMapper = {
|
|
11
|
-
emitMappedEvent: jest.fn(),
|
|
12
|
-
emit: jest.fn()
|
|
13
|
-
} as any;
|
|
14
|
-
handler = new DGWHandler(eventMapper);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should handle a valid DGW frame with insertMessage", () => {
|
|
18
|
-
const frame = {
|
|
19
|
-
payloadJson: JSON.stringify({
|
|
20
|
-
payload: JSON.stringify([
|
|
21
|
-
5, "insertMessage",
|
|
22
|
-
"hello from dgw", // 0: text
|
|
23
|
-
null, null,
|
|
24
|
-
"123456", // 3: chatJid
|
|
25
|
-
null,
|
|
26
|
-
1600000000000, // 5: timestamp
|
|
27
|
-
null, null,
|
|
28
|
-
"mid.dgw.1", // 8: messageId
|
|
29
|
-
null,
|
|
30
|
-
"1001" // 10: senderJid
|
|
31
|
-
])
|
|
32
|
-
})
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
handler.handleDGWFrame(frame as any);
|
|
36
|
-
|
|
37
|
-
expect(eventMapper.emitMappedEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
38
|
-
type: "e2ee_message",
|
|
39
|
-
data: expect.objectContaining({
|
|
40
|
-
messageId: "mid.dgw.1",
|
|
41
|
-
text: "hello from dgw"
|
|
42
|
-
})
|
|
43
|
-
}));
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("should ignore duplicate message IDs", () => {
|
|
47
|
-
const frame = {
|
|
48
|
-
payloadJson: JSON.stringify({
|
|
49
|
-
payload: JSON.stringify([
|
|
50
|
-
5, "insertMessage", "hello", null, null, "123", null, 123, null, null, "mid.dup", null, "100"
|
|
51
|
-
])
|
|
52
|
-
})
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
handler.handleDGWFrame(frame as any);
|
|
56
|
-
handler.handleDGWFrame(frame as any);
|
|
57
|
-
|
|
58
|
-
expect(eventMapper.emitMappedEvent).toHaveBeenCalledTimes(1);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
-
import { E2EEHandler } from "../../../src/controllers/e2ee-handler.ts";
|
|
3
|
-
import { EventMapper } from "../../../src/controllers/event-mapper.ts";
|
|
4
|
-
import { DeviceStore } from "../../../src/e2ee/store/device-store.ts";
|
|
5
|
-
import { unmarshal, type Node } from "../../../src/e2ee/transport/binary/wa-binary.ts";
|
|
6
|
-
import {
|
|
7
|
-
encodeImageMessage,
|
|
8
|
-
encodeMessageApplication,
|
|
9
|
-
encodeMessageTransport,
|
|
10
|
-
encodeRevokeMessage,
|
|
11
|
-
encodeTextMessage,
|
|
12
|
-
} from "../../../src/e2ee/message/message-builder.ts";
|
|
13
|
-
|
|
14
|
-
const buildTransport = (consumerApplication: Buffer) => {
|
|
15
|
-
const { messageApp } = encodeMessageApplication(consumerApplication);
|
|
16
|
-
return encodeMessageTransport({ messageApp, padding: Buffer.from([1]) });
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
describe("E2EEHandler", () => {
|
|
20
|
-
let eventMapper: EventMapper;
|
|
21
|
-
let socket: any;
|
|
22
|
-
let store: any;
|
|
23
|
-
let handler: E2EEHandler;
|
|
24
|
-
let logSpy: jest.SpiedFunction<typeof console.log>;
|
|
25
|
-
let warnSpy: jest.SpiedFunction<typeof console.warn>;
|
|
26
|
-
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
29
|
-
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
30
|
-
eventMapper = {
|
|
31
|
-
emitMappedEvent: jest.fn(),
|
|
32
|
-
emit: jest.fn()
|
|
33
|
-
} as any;
|
|
34
|
-
socket = {
|
|
35
|
-
sendFrame: jest.fn<() => Promise<void>>().mockResolvedValue(undefined as any)
|
|
36
|
-
};
|
|
37
|
-
store = { jidDevice: 1, registrationId: 1234 };
|
|
38
|
-
handler = new E2EEHandler(
|
|
39
|
-
eventMapper,
|
|
40
|
-
() => socket,
|
|
41
|
-
() => store
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
afterEach(() => {
|
|
46
|
-
logSpy.mockRestore();
|
|
47
|
-
warnSpy.mockRestore();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("should handle IQ ping", () => {
|
|
51
|
-
const node = {
|
|
52
|
-
tag: "iq",
|
|
53
|
-
attrs: { id: "123", xmlns: "urn:xmpp:ping", type: "get", from: "s.whatsapp.net" },
|
|
54
|
-
content: undefined
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
handler.handleIQ(node as any);
|
|
58
|
-
|
|
59
|
-
expect(socket.sendFrame).toHaveBeenCalled();
|
|
60
|
-
const mock = (socket.sendFrame as any);
|
|
61
|
-
expect(mock.mock.calls[0][0]).toBeDefined();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("should decrypt and emit normalized DM text messages", async () => {
|
|
65
|
-
const plaintext = buildTransport(encodeTextMessage("hello"));
|
|
66
|
-
const node = {
|
|
67
|
-
tag: "message",
|
|
68
|
-
attrs: { id: "mid.1", from: "1001.2@msgr", type: "chat" },
|
|
69
|
-
content: [
|
|
70
|
-
{ tag: "enc", attrs: { v: "3", type: "msg" }, content: Buffer.from("ciphertext") }
|
|
71
|
-
]
|
|
72
|
-
};
|
|
73
|
-
const e2eeClient = {
|
|
74
|
-
decryptDMMessage: jest.fn<(senderJid: string, ciphertext: Buffer) => Promise<Buffer>>().mockResolvedValue(plaintext as any)
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
await handler.handleEncryptedMessage(node as any, "self_id", e2eeClient as any);
|
|
78
|
-
|
|
79
|
-
expect(e2eeClient.decryptDMMessage).toHaveBeenCalledWith("1001.2@msgr", Buffer.from("ciphertext"));
|
|
80
|
-
expect(eventMapper.emitMappedEvent).toHaveBeenCalledWith({
|
|
81
|
-
type: "e2ee_message",
|
|
82
|
-
data: expect.objectContaining({
|
|
83
|
-
kind: "text",
|
|
84
|
-
text: "hello",
|
|
85
|
-
messageId: "mid.1",
|
|
86
|
-
senderJid: "1001.2@msgr",
|
|
87
|
-
senderId: "1001",
|
|
88
|
-
}),
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("should normalize application-data revoke messages", async () => {
|
|
93
|
-
const plaintext = buildTransport(encodeRevokeMessage("mid.remove", true));
|
|
94
|
-
const node = {
|
|
95
|
-
tag: "message",
|
|
96
|
-
attrs: { id: "mid.revoke", from: "1001.2@msgr", type: "chat" },
|
|
97
|
-
content: [
|
|
98
|
-
{ tag: "enc", attrs: { v: "3", type: "msg" }, content: Buffer.from("ciphertext") }
|
|
99
|
-
]
|
|
100
|
-
};
|
|
101
|
-
const e2eeClient = {
|
|
102
|
-
decryptDMMessage: jest.fn<(senderJid: string, ciphertext: Buffer) => Promise<Buffer>>().mockResolvedValue(plaintext as any)
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
await handler.handleEncryptedMessage(node as any, "self_id", e2eeClient as any);
|
|
106
|
-
|
|
107
|
-
expect(eventMapper.emitMappedEvent).toHaveBeenCalledWith({
|
|
108
|
-
type: "e2ee_message",
|
|
109
|
-
data: expect.objectContaining({
|
|
110
|
-
kind: "revoke",
|
|
111
|
-
targetId: "mid.remove",
|
|
112
|
-
fromMe: true,
|
|
113
|
-
}),
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("should emit media messages after decoding the ConsumerApplication wrapper", async () => {
|
|
118
|
-
const plaintext = buildTransport(encodeImageMessage({
|
|
119
|
-
mimeType: "image/png",
|
|
120
|
-
fileSHA256: Buffer.alloc(32, 1),
|
|
121
|
-
fileLength: 10,
|
|
122
|
-
mediaKey: Buffer.alloc(32, 2),
|
|
123
|
-
fileEncSHA256: Buffer.alloc(32, 3),
|
|
124
|
-
directPath: "/mms/image",
|
|
125
|
-
caption: "photo caption",
|
|
126
|
-
}));
|
|
127
|
-
const node = {
|
|
128
|
-
tag: "message",
|
|
129
|
-
attrs: { id: "mid.image", from: "1001.2@msgr", type: "chat" },
|
|
130
|
-
content: [
|
|
131
|
-
{ tag: "enc", attrs: { v: "3", type: "msg" }, content: Buffer.from("ciphertext") }
|
|
132
|
-
]
|
|
133
|
-
};
|
|
134
|
-
const e2eeClient = {
|
|
135
|
-
decryptDMMessage: jest.fn<(senderJid: string, ciphertext: Buffer) => Promise<Buffer>>().mockResolvedValue(plaintext as any)
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
await handler.handleEncryptedMessage(node as any, "self_id", e2eeClient as any);
|
|
139
|
-
|
|
140
|
-
expect(eventMapper.emitMappedEvent).toHaveBeenCalledWith({
|
|
141
|
-
type: "e2ee_message",
|
|
142
|
-
data: expect.objectContaining({
|
|
143
|
-
kind: "image",
|
|
144
|
-
media: expect.objectContaining({
|
|
145
|
-
caption: expect.objectContaining({ text: "photo caption" }),
|
|
146
|
-
image: expect.objectContaining({ version: 1 }),
|
|
147
|
-
}),
|
|
148
|
-
}),
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("processes participant SKDM fanout before acknowledging a group message without main enc", async () => {
|
|
153
|
-
const skdmBytes = Buffer.from("skdm-bytes");
|
|
154
|
-
const participantTransport = encodeMessageTransport({
|
|
155
|
-
padding: Buffer.from([1]),
|
|
156
|
-
skdm: { groupId: "180@g.us", skdmBytes },
|
|
157
|
-
});
|
|
158
|
-
const node = {
|
|
159
|
-
tag: "message",
|
|
160
|
-
attrs: { id: "mid.skdm", from: "180@g.us", participant: "1001.2@msgr" },
|
|
161
|
-
content: [
|
|
162
|
-
{
|
|
163
|
-
tag: "participants",
|
|
164
|
-
attrs: {},
|
|
165
|
-
content: [
|
|
166
|
-
{
|
|
167
|
-
tag: "to",
|
|
168
|
-
attrs: { jid: "self:1@msgr" },
|
|
169
|
-
content: [{ tag: "enc", attrs: { type: "msg" }, content: Buffer.from("participant-ciphertext") }],
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
};
|
|
175
|
-
const e2eeClient = {
|
|
176
|
-
decryptDMMessage: jest.fn<(senderJid: string, ciphertext: Buffer) => Promise<Buffer>>().mockResolvedValue(participantTransport as any),
|
|
177
|
-
processSenderKeyDistribution: jest.fn<(senderJid: string, skdm: Buffer, groupJid?: string) => Promise<void>>().mockResolvedValue(undefined),
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
await handler.handleEncryptedMessage(node as any, "self", e2eeClient as any);
|
|
181
|
-
|
|
182
|
-
expect(e2eeClient.decryptDMMessage).toHaveBeenCalledWith("1001.2@msgr", Buffer.from("participant-ciphertext"));
|
|
183
|
-
expect(e2eeClient.processSenderKeyDistribution).toHaveBeenCalledWith("1001.2@msgr", skdmBytes, "180@g.us");
|
|
184
|
-
expect(unmarshal(socket.sendFrame.mock.calls.at(-1)![0] as Buffer)).toMatchObject({
|
|
185
|
-
tag: "ack",
|
|
186
|
-
attrs: { class: "message", id: "mid.skdm", to: "180@g.us", participant: "1001.2@msgr" },
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("sends retry receipts for unavailable encrypted group messages", async () => {
|
|
191
|
-
const node = {
|
|
192
|
-
tag: "message",
|
|
193
|
-
attrs: { id: "mid.unavailable", from: "180@g.us", participant: "1001.2@msgr", t: "123" },
|
|
194
|
-
content: [{ tag: "unavailable", attrs: { type: "skmsg" } }],
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
await handler.handleEncryptedMessage(node as any, "self", {} as any);
|
|
198
|
-
|
|
199
|
-
const receipt = unmarshal(socket.sendFrame.mock.calls[0][0] as Buffer);
|
|
200
|
-
const ack = unmarshal(socket.sendFrame.mock.calls.at(-1)![0] as Buffer);
|
|
201
|
-
expect(receipt).toMatchObject({
|
|
202
|
-
tag: "receipt",
|
|
203
|
-
attrs: { id: "mid.unavailable", to: "180@g.us", type: "retry", participant: "1001.2@msgr" },
|
|
204
|
-
});
|
|
205
|
-
const receiptChildren = receipt.content as Node[];
|
|
206
|
-
expect(receiptChildren.find(n => n.tag === "retry")).toMatchObject({
|
|
207
|
-
attrs: { count: "1", id: "mid.unavailable", t: "123", v: "1" },
|
|
208
|
-
});
|
|
209
|
-
expect(receiptChildren.find(n => n.tag === "registration")!.content).toEqual(Buffer.from([0, 0, 4, 210]));
|
|
210
|
-
expect(ack).toMatchObject({
|
|
211
|
-
tag: "ack",
|
|
212
|
-
attrs: { class: "message", id: "mid.unavailable", to: "180@g.us", participant: "1001.2@msgr" },
|
|
213
|
-
});
|
|
214
|
-
expect(eventMapper.emitMappedEvent).toHaveBeenCalledWith({
|
|
215
|
-
type: "e2ee_message",
|
|
216
|
-
data: expect.objectContaining({
|
|
217
|
-
type: "decryption_failed",
|
|
218
|
-
error: "unavailable encrypted message: skmsg",
|
|
219
|
-
messageId: "mid.unavailable",
|
|
220
|
-
}),
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("caps retry receipts per message", async () => {
|
|
225
|
-
const node = {
|
|
226
|
-
tag: "message",
|
|
227
|
-
attrs: { id: "mid.retry-cap", from: "180@g.us", participant: "1001.2@msgr" },
|
|
228
|
-
content: [{ tag: "unavailable", attrs: { type: "skmsg" } }],
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
await handler.handleEncryptedMessage(node as any, "self", {} as any);
|
|
232
|
-
await handler.handleEncryptedMessage(node as any, "self", {} as any);
|
|
233
|
-
await handler.handleEncryptedMessage(node as any, "self", {} as any);
|
|
234
|
-
|
|
235
|
-
const sentNodes = socket.sendFrame.mock.calls.map((call: any[]) => unmarshal(call[0]));
|
|
236
|
-
expect(sentNodes.filter((n: Node) => n.tag === "receipt")).toHaveLength(2);
|
|
237
|
-
expect(sentNodes.filter((n: Node) => n.tag === "ack")).toHaveLength(3);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("uploads fresh prekeys when encrypt notification reports a low count", async () => {
|
|
241
|
-
store = await DeviceStore.memoryOnly();
|
|
242
|
-
handler = new E2EEHandler(eventMapper, () => socket, () => store);
|
|
243
|
-
|
|
244
|
-
await handler.handleNotification({
|
|
245
|
-
tag: "notification",
|
|
246
|
-
attrs: { type: "encrypt", from: "s.whatsapp.net" },
|
|
247
|
-
content: [{ tag: "count", attrs: { value: "3" } }],
|
|
248
|
-
} as any);
|
|
249
|
-
|
|
250
|
-
const iq = unmarshal(socket.sendFrame.mock.calls[0][0] as Buffer);
|
|
251
|
-
const children = iq.content as Node[];
|
|
252
|
-
const list = children.find(n => n.tag === "list")!;
|
|
253
|
-
expect(iq).toMatchObject({ tag: "iq", attrs: { to: "s.whatsapp.net", type: "set", xmlns: "encrypt" } });
|
|
254
|
-
expect(children.find(n => n.tag === "registration")!.content).toHaveLength(4);
|
|
255
|
-
expect(children.find(n => n.tag === "identity")!.content).toHaveLength(32);
|
|
256
|
-
expect((list.content as Node[])).toHaveLength(50);
|
|
257
|
-
expect(children.find(n => n.tag === "skey")).toBeDefined();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
it("fetches media upload config from media_conn IQ result", async () => {
|
|
262
|
-
let sentIq: Node | null = null;
|
|
263
|
-
socket.sendFrame = jest.fn<(frame: Buffer) => Promise<void>>().mockImplementation(async (frame: Buffer) => {
|
|
264
|
-
sentIq = unmarshal(frame);
|
|
265
|
-
const id = sentIq.attrs.id;
|
|
266
|
-
handler.handleIQ({
|
|
267
|
-
tag: "iq",
|
|
268
|
-
attrs: { id, type: "result", from: "s.whatsapp.net" },
|
|
269
|
-
content: [{
|
|
270
|
-
tag: "media_conn",
|
|
271
|
-
attrs: { auth: "real-auth", ttl: "3600", auth_ttl: "7200", max_buckets: "20" },
|
|
272
|
-
content: [
|
|
273
|
-
{ tag: "host", attrs: { hostname: "primary.example" } },
|
|
274
|
-
{ tag: "host", attrs: { hostname: "rupload.facebook.com" } },
|
|
275
|
-
],
|
|
276
|
-
}],
|
|
277
|
-
} as any);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
await expect(handler.getMediaUploadConfig()).resolves.toMatchObject({
|
|
281
|
-
host: "rupload.facebook.com",
|
|
282
|
-
auth: "real-auth",
|
|
283
|
-
ttl: 3600,
|
|
284
|
-
authTtl: 7200,
|
|
285
|
-
});
|
|
286
|
-
expect(sentIq).toMatchObject({
|
|
287
|
-
tag: "iq",
|
|
288
|
-
attrs: { to: "s.whatsapp.net", type: "set", xmlns: "w:m" },
|
|
289
|
-
});
|
|
290
|
-
expect((sentIq!.content as Node[])[0]).toMatchObject({ tag: "media_conn", attrs: {} });
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
});
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
import { EventMapper } from "../../../src/controllers/event-mapper.ts";
|
|
4
|
-
import { MediaService } from "../../../src/services/media.service.ts";
|
|
5
|
-
import { E2EEService } from "../../../src/services/e2ee.service.ts";
|
|
6
|
-
|
|
7
|
-
describe("EventMapper", () => {
|
|
8
|
-
let eventBus: EventEmitter;
|
|
9
|
-
let mediaService: MediaService;
|
|
10
|
-
let e2eeService: E2EEService;
|
|
11
|
-
let mapper: EventMapper;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
eventBus = new EventEmitter();
|
|
15
|
-
// Mock services
|
|
16
|
-
mediaService = {
|
|
17
|
-
normalizeAttachment: jest.fn().mockImplementation(item => item)
|
|
18
|
-
} as any;
|
|
19
|
-
e2eeService = {
|
|
20
|
-
markConnected: jest.fn()
|
|
21
|
-
} as any;
|
|
22
|
-
mapper = new EventMapper(eventBus, mediaService, e2eeService);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("should map a simple text message", (done) => {
|
|
26
|
-
const raw = {
|
|
27
|
-
type: "message",
|
|
28
|
-
messageID: "mid.123",
|
|
29
|
-
threadID: "1000",
|
|
30
|
-
senderID: "1001",
|
|
31
|
-
body: "hello world",
|
|
32
|
-
timestamp: 1600000000000,
|
|
33
|
-
attachments: []
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
eventBus.on("message", (data) => {
|
|
37
|
-
expect(data.id).toBe("mid.123");
|
|
38
|
-
expect(data.text).toBe("hello world");
|
|
39
|
-
expect(data.senderId).toBe("1001");
|
|
40
|
-
done();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
mapper.emitMappedEvent(raw);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("should map a message reply", (done) => {
|
|
47
|
-
const raw = {
|
|
48
|
-
type: "message_reply",
|
|
49
|
-
messageID: "mid.reply",
|
|
50
|
-
threadID: "1000",
|
|
51
|
-
senderID: "1001",
|
|
52
|
-
body: "replying",
|
|
53
|
-
messageReply: {
|
|
54
|
-
messageID: "mid.original",
|
|
55
|
-
senderID: "1002",
|
|
56
|
-
body: "original content"
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
eventBus.on("message", (data) => {
|
|
61
|
-
expect(data.replyTo).toBeDefined();
|
|
62
|
-
expect(data.replyTo.messageId).toBe("mid.original");
|
|
63
|
-
done();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
mapper.emitMappedEvent(raw);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("should map a message edit", (done) => {
|
|
70
|
-
const raw = {
|
|
71
|
-
type: "message_edit",
|
|
72
|
-
messageID: "mid.123",
|
|
73
|
-
threadID: "1000",
|
|
74
|
-
newText: "edited text",
|
|
75
|
-
editCount: 1
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
eventBus.on("messageEdit", (data) => {
|
|
79
|
-
expect(data.newText).toBe("edited text");
|
|
80
|
-
done();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
mapper.emitMappedEvent(raw);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("should map a reaction", (done) => {
|
|
87
|
-
const raw = {
|
|
88
|
-
type: "reaction",
|
|
89
|
-
messageID: "mid.123",
|
|
90
|
-
threadID: "1000",
|
|
91
|
-
senderID: "1001",
|
|
92
|
-
reaction: "👍"
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
eventBus.on("reaction", (data) => {
|
|
96
|
-
expect(data.reaction).toBe("👍");
|
|
97
|
-
expect(data.actorId).toBe("1001");
|
|
98
|
-
done();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
mapper.emitMappedEvent(raw);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("should map typing status", (done) => {
|
|
105
|
-
const raw = {
|
|
106
|
-
type: "typ",
|
|
107
|
-
threadID: "1000",
|
|
108
|
-
from: "1001",
|
|
109
|
-
isTyping: true
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
eventBus.on("typing", (data) => {
|
|
113
|
-
expect(data.isTyping).toBe(true);
|
|
114
|
-
expect(data.senderId).toBe("1001");
|
|
115
|
-
done();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
mapper.emitMappedEvent(raw);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("should normalize E2EE message identity and omit empty optional fields", (done) => {
|
|
122
|
-
eventBus.on("e2ee_message", (data) => {
|
|
123
|
-
expect(data.id).toBe("7456191609143713633");
|
|
124
|
-
expect(data.threadId).toBe("100042415119261");
|
|
125
|
-
expect(data.chatJid).toBe("100042415119261.0@msgr");
|
|
126
|
-
expect(data.senderJid).toBe("100042415119261.160@msgr");
|
|
127
|
-
expect(data.senderId).toBe("100042415119261");
|
|
128
|
-
expect(data.senderDeviceId).toBe(160);
|
|
129
|
-
expect(data.isGroup).toBe(false);
|
|
130
|
-
expect(data.kind).toBe("text");
|
|
131
|
-
expect(data.text).toBe("Hehe");
|
|
132
|
-
expect(Object.prototype.hasOwnProperty.call(data, "attachments")).toBe(false);
|
|
133
|
-
expect(Object.prototype.hasOwnProperty.call(data, "mentions")).toBe(false);
|
|
134
|
-
expect(Object.prototype.hasOwnProperty.call(data, "replyTo")).toBe(false);
|
|
135
|
-
done();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
mapper.emitMappedEvent({
|
|
139
|
-
type: "e2ee_message",
|
|
140
|
-
data: {
|
|
141
|
-
messageId: "7456191609143713633",
|
|
142
|
-
chatJid: "100042415119261.160@msgr",
|
|
143
|
-
senderJid: "100042415119261.160@msgr",
|
|
144
|
-
kind: "text",
|
|
145
|
-
text: "Hehe",
|
|
146
|
-
timestampMs: 1777694609888,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("should preserve group chat JID and sender device for E2EE group messages", (done) => {
|
|
152
|
-
eventBus.on("e2ee_message", (data) => {
|
|
153
|
-
expect(data.threadId).toBe("1805602490133470@g.us");
|
|
154
|
-
expect(data.chatJid).toBe("1805602490133470@g.us");
|
|
155
|
-
expect(data.senderJid).toBe("100042415119261.101@msgr");
|
|
156
|
-
expect(data.senderId).toBe("100042415119261");
|
|
157
|
-
expect(data.senderDeviceId).toBe(101);
|
|
158
|
-
expect(data.isGroup).toBe(true);
|
|
159
|
-
expect(data.kind).toBe("text");
|
|
160
|
-
done();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
mapper.emitMappedEvent({
|
|
164
|
-
type: "e2ee_message",
|
|
165
|
-
data: {
|
|
166
|
-
messageId: "m1",
|
|
167
|
-
chatJid: "1805602490133470@g.us",
|
|
168
|
-
senderJid: "100042415119261.101@msgr",
|
|
169
|
-
type: "text",
|
|
170
|
-
text: "hello group",
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it("should route E2EE decrypt failures through catch-all without unhandled error", (done) => {
|
|
176
|
-
eventBus.on("event", (event) => {
|
|
177
|
-
expect(event.type).toBe("error");
|
|
178
|
-
expect(event.data.message).toContain("E2EE decrypt failed");
|
|
179
|
-
done();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
expect(() => mapper.emitMappedEvent({
|
|
183
|
-
type: "e2ee_message",
|
|
184
|
-
data: {
|
|
185
|
-
type: "decryption_failed",
|
|
186
|
-
chatJid: "1805602490133470@g.us",
|
|
187
|
-
senderJid: "100042415119261.101@msgr",
|
|
188
|
-
error: "missing sender key state",
|
|
189
|
-
},
|
|
190
|
-
})).not.toThrow();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
it("should mark E2EE connected and emit both typed and catch-all events", (done) => {
|
|
195
|
-
const seen: string[] = [];
|
|
196
|
-
eventBus.on("e2ee_connected", () => seen.push("typed"));
|
|
197
|
-
eventBus.on("event", (event) => {
|
|
198
|
-
if (event.type !== "e2ee_connected") return;
|
|
199
|
-
seen.push("catch-all");
|
|
200
|
-
expect(e2eeService.markConnected).toHaveBeenCalledTimes(1);
|
|
201
|
-
expect(seen).toEqual(["typed", "catch-all"]);
|
|
202
|
-
done();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
mapper.emitMappedEvent({ type: "e2eeConnected" });
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("should infer E2EE media kind and preserve optional payload fields", (done) => {
|
|
209
|
-
eventBus.on("e2ee_message", (data) => {
|
|
210
|
-
expect(data.kind).toBe("image");
|
|
211
|
-
expect(data.media).toEqual({ type: "image", directPath: "/m" });
|
|
212
|
-
expect(data.reaction).toBe("🔥");
|
|
213
|
-
expect(data.targetId).toBe("mid.target");
|
|
214
|
-
expect(data.fromMe).toBe(true);
|
|
215
|
-
expect(data.replyTo).toEqual({ messageId: "mid.reply", senderId: "200" });
|
|
216
|
-
done();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
mapper.emitMappedEvent({
|
|
220
|
-
type: "e2ee_message",
|
|
221
|
-
data: {
|
|
222
|
-
messageId: "mid.media",
|
|
223
|
-
chatJid: "100.0@msgr",
|
|
224
|
-
senderJid: "100.5@msgr",
|
|
225
|
-
media: { type: "image", directPath: "/m" },
|
|
226
|
-
emoji: "🔥",
|
|
227
|
-
targetId: "mid.target",
|
|
228
|
-
fromMe: true,
|
|
229
|
-
replyToId: "mid.reply",
|
|
230
|
-
replyToSenderJid: "200.9@msgr",
|
|
231
|
-
},
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("should emit typed error events when an error listener exists", (done) => {
|
|
236
|
-
let catchAllSeen = false;
|
|
237
|
-
eventBus.on("event", (event) => {
|
|
238
|
-
if (event.type === "error") catchAllSeen = true;
|
|
239
|
-
});
|
|
240
|
-
eventBus.on("error", (data) => {
|
|
241
|
-
expect(data.message).toBe("boom");
|
|
242
|
-
expect(catchAllSeen).toBe(false);
|
|
243
|
-
setImmediate(() => {
|
|
244
|
-
expect(catchAllSeen).toBe(true);
|
|
245
|
-
done();
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
mapper.emit({ type: "error", data: { message: "boom" } });
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
});
|