fb-messenger-e2ee 0.1.1 → 0.1.2

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