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.
Files changed (111) hide show
  1. package/DOCS.md +41 -3
  2. package/README.md +27 -2
  3. package/dist/index.cjs +9017 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +910 -0
  6. package/dist/index.d.ts +910 -0
  7. package/dist/index.js +9049 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +19 -4
  10. package/bun.lock +0 -1014
  11. package/examples/basic-usage.ts +0 -52
  12. package/jest.config.ts +0 -18
  13. package/src/config/env.ts +0 -15
  14. package/src/controllers/client.controller.ts +0 -805
  15. package/src/controllers/dgw-handler.ts +0 -171
  16. package/src/controllers/e2ee-handler.ts +0 -832
  17. package/src/controllers/event-mapper.ts +0 -327
  18. package/src/core/client.ts +0 -151
  19. package/src/e2ee/application/e2ee-client.ts +0 -376
  20. package/src/e2ee/application/fanout-planner.ts +0 -79
  21. package/src/e2ee/application/outbound-message-cache.ts +0 -58
  22. package/src/e2ee/application/prekey-maintenance.ts +0 -55
  23. package/src/e2ee/application/retry-manager.ts +0 -156
  24. package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
  25. package/src/e2ee/facebook/icdc-payload.ts +0 -31
  26. package/src/e2ee/index.ts +0 -76
  27. package/src/e2ee/internal.ts +0 -27
  28. package/src/e2ee/media/media-crypto.ts +0 -147
  29. package/src/e2ee/media/media-upload.ts +0 -90
  30. package/src/e2ee/message/builders/client-payload.ts +0 -54
  31. package/src/e2ee/message/builders/consumer-application.ts +0 -362
  32. package/src/e2ee/message/builders/message-application.ts +0 -64
  33. package/src/e2ee/message/builders/message-transport.ts +0 -84
  34. package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
  35. package/src/e2ee/message/constants.ts +0 -4
  36. package/src/e2ee/message/message-builder.ts +0 -15
  37. package/src/e2ee/signal/prekey-manager.ts +0 -140
  38. package/src/e2ee/signal/signal-manager.ts +0 -365
  39. package/src/e2ee/store/device-json.ts +0 -47
  40. package/src/e2ee/store/device-repository.ts +0 -12
  41. package/src/e2ee/store/device-store.ts +0 -538
  42. package/src/e2ee/transport/binary/decoder.ts +0 -180
  43. package/src/e2ee/transport/binary/encoder.ts +0 -143
  44. package/src/e2ee/transport/binary/stanzas.ts +0 -97
  45. package/src/e2ee/transport/binary/tokens.ts +0 -64
  46. package/src/e2ee/transport/binary/wa-binary.ts +0 -8
  47. package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
  48. package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
  49. package/src/e2ee/transport/noise/noise-socket.ts +0 -230
  50. package/src/index.ts +0 -34
  51. package/src/models/client.ts +0 -26
  52. package/src/models/config.ts +0 -14
  53. package/src/models/domain.ts +0 -299
  54. package/src/models/e2ee.ts +0 -295
  55. package/src/models/media.ts +0 -41
  56. package/src/models/messaging.ts +0 -88
  57. package/src/models/thread.ts +0 -69
  58. package/src/repositories/session.repository.ts +0 -20
  59. package/src/services/auth.service.ts +0 -55
  60. package/src/services/e2ee.service.ts +0 -174
  61. package/src/services/facebook-gateway.service.ts +0 -245
  62. package/src/services/icdc.service.ts +0 -177
  63. package/src/services/media.service.ts +0 -304
  64. package/src/services/messaging.service.ts +0 -28
  65. package/src/services/thread.service.ts +0 -199
  66. package/src/types/advanced-types.ts +0 -61
  67. package/src/types/fca-unofficial.d.ts +0 -212
  68. package/src/types/protocol-types.ts +0 -43
  69. package/src/utils/fca-utils.ts +0 -30
  70. package/src/utils/logger.ts +0 -24
  71. package/src/utils/mime.ts +0 -51
  72. package/tests/.env.example +0 -87
  73. package/tests/data/1x1.png +0 -0
  74. package/tests/data/example.txt +0 -1
  75. package/tests/data/file_example.mp3 +0 -0
  76. package/tests/data/file_example.mp4 +0 -0
  77. package/tests/integration.test.ts +0 -498
  78. package/tests/script/echo-e2ee.ts +0 -174
  79. package/tests/script/send-e2ee-media.ts +0 -227
  80. package/tests/script/send-e2ee-reaction.ts +0 -108
  81. package/tests/script/send-e2ee-unsend.ts +0 -115
  82. package/tests/script/send-typing.ts +0 -107
  83. package/tests/script/test-group-send.ts +0 -105
  84. package/tests/setup.ts +0 -3
  85. package/tests/types.test.ts +0 -57
  86. package/tests/unit/controllers/dgw-handler.test.ts +0 -60
  87. package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
  88. package/tests/unit/controllers/event-mapper.test.ts +0 -252
  89. package/tests/unit/e2ee/application-helpers.test.ts +0 -418
  90. package/tests/unit/e2ee/device-store.test.ts +0 -126
  91. package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
  92. package/tests/unit/e2ee/media-crypto.test.ts +0 -55
  93. package/tests/unit/e2ee/media-upload.test.ts +0 -60
  94. package/tests/unit/e2ee/message-builder.test.ts +0 -42
  95. package/tests/unit/e2ee/message-builders.test.ts +0 -230
  96. package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
  97. package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
  98. package/tests/unit/e2ee/wa-binary.test.ts +0 -127
  99. package/tests/unit/services/e2ee.service.test.ts +0 -101
  100. package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
  101. package/tests/unit/services/media.service.test.ts +0 -169
  102. package/tests/unit/utils/fca-utils.test.ts +0 -48
  103. package/tsconfig.json +0 -19
  104. /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
  105. /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
  106. /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
  107. /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
  108. /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
  109. /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
  110. /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
  111. /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
@@ -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
- });