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,177 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { PrivateKey } from "@signalapp/libsignal-client";
3
- import { encodeICDCIdentityList, encodeSignedICDCIdentityList } from "../e2ee/facebook/icdc-payload.ts";
4
- import type { DeviceStore } from "../e2ee/store/device-store.ts";
5
- import { logger } from "../utils/logger.ts";
6
-
7
- export interface ICDCFetchResponse {
8
- device_identities: string[];
9
- icdc_seq: number;
10
- status: number;
11
- }
12
-
13
- export interface ICDCRegisterResponse {
14
- icdc_success?: boolean;
15
- product?: string;
16
- status: number;
17
- type?: string;
18
- wa_device_id: number;
19
- }
20
-
21
- export class ICDCService {
22
- private readonly baseUrl = "https://reg-e2ee.facebook.com/v2";
23
- private readonly origin = "https://www.messenger.com";
24
- private readonly referer = "https://www.messenger.com/messages/";
25
- private cookies: string = "";
26
-
27
- constructor(
28
- private readonly userAgent: string,
29
- initialCookies?: string,
30
- ) {
31
- if (initialCookies) this.cookies = initialCookies;
32
- }
33
-
34
- public setCookies(cookies: string): void {
35
- this.cookies = cookies;
36
- }
37
-
38
- public async register(
39
- fbid: string,
40
- fbCat: string,
41
- appId: string,
42
- deviceStore: DeviceStore,
43
- ): Promise<number> {
44
- logger.debug("ICDCService", "Starting ICDC registration...");
45
-
46
- // Fetch current ICDC state
47
- const fetchResp = await this.fetchICDC(fbid, deviceStore.facebookUUID, appId, fbCat);
48
- if (fetchResp.status !== 200) {
49
- throw new Error(`ICDC fetch failed with status ${fetchResp.status}`);
50
- }
51
-
52
- const deviceIdentities = fetchResp.device_identities.map((id) =>
53
- Buffer.from(id, "base64")
54
- );
55
-
56
- // Check if our own identity is already there
57
- const ownIdentityPub = deviceStore.getIdentityPublicKey();
58
- let ownIdentityIndex = deviceIdentities.findIndex((id) =>
59
- Buffer.compare(id, ownIdentityPub) === 0
60
- );
61
-
62
- let nextSeq = fetchResp.icdc_seq;
63
- if (ownIdentityIndex === -1) {
64
- logger.debug("ICDCService", "Own identity not found in list, adding...");
65
- ownIdentityIndex = deviceIdentities.length;
66
- deviceIdentities.push(Buffer.from(ownIdentityPub));
67
- nextSeq++;
68
- } else {
69
- logger.debug("ICDCService", `Own identity found at index ${ownIdentityIndex}`);
70
- }
71
-
72
- const icdcTs = Math.floor(Date.now() / 1000);
73
-
74
- // Build and sign ICDC Identity List
75
- const unsignedList = encodeICDCIdentityList({
76
- seq: nextSeq,
77
- timestamp: icdcTs,
78
- devices: deviceIdentities,
79
- signingDeviceIndex: ownIdentityIndex,
80
- });
81
-
82
- const privKey = PrivateKey.deserialize(Buffer.from(deviceStore.getIdentityPrivateKey()));
83
- const signature = Buffer.from(privKey.sign(unsignedList));
84
-
85
- const signedList = encodeSignedICDCIdentityList({
86
- details: unsignedList,
87
- signature: signature,
88
- });
89
-
90
- // Register
91
- const identitiesHash = this.calculateIdentitiesHash(deviceIdentities);
92
-
93
- const form = new URLSearchParams();
94
- form.set("fbid", fbid);
95
- form.set("fb_cat", fbCat);
96
- form.set("app_id", appId);
97
- form.set("device_id", deviceStore.facebookUUID);
98
-
99
- // Registration ID: big-endian uint32
100
- const regIdBuf = Buffer.alloc(4);
101
- regIdBuf.writeUInt32BE(deviceStore.registrationId);
102
- form.set("e_regid", regIdBuf.toString("base64"));
103
-
104
- form.set("e_keytype", Buffer.from([0x05]).toString("base64")); // DJB_TYPE
105
- form.set("e_ident", ownIdentityPub.toString("base64"));
106
-
107
- // Signed Prekey ID: big-endian uint24 (last 3 bytes of uint32)
108
- const skeyIdBuf = Buffer.alloc(4);
109
- skeyIdBuf.writeUInt32BE(deviceStore.signedPreKeyId);
110
- form.set("e_skey_id", skeyIdBuf.subarray(1).toString("base64"));
111
-
112
- form.set("e_skey_val", deviceStore.getSignedPreKeyPublicKey().toString("base64"));
113
- form.set("e_skey_sig", deviceStore.signedPreKeySig.toString("base64"));
114
-
115
- form.set("icdc_list", signedList.toString("base64"));
116
- form.set("icdc_ts", icdcTs.toString());
117
- form.set("icdc_seq", nextSeq.toString());
118
- form.set("ihash", identitiesHash.toString("base64"));
119
-
120
- const registerResp = await this.post<ICDCRegisterResponse>(
121
- "fb_register_v2",
122
- form
123
- );
124
-
125
- if (registerResp.status !== 200) {
126
- throw new Error(`ICDC register failed with status ${registerResp.status}`);
127
- }
128
-
129
- logger.info("ICDCService", `ICDC registration successful. WA Device ID: ${registerResp.wa_device_id}`);
130
- return registerResp.wa_device_id;
131
- }
132
-
133
- private async fetchICDC(fbid: string, facebookUUID: string, appId: string, fbCat: string): Promise<ICDCFetchResponse> {
134
- const form = new URLSearchParams();
135
- form.set("fbid", fbid);
136
- form.set("device_id", facebookUUID);
137
- form.set("app_id", appId);
138
- form.set("fb_cat", fbCat);
139
-
140
- return this.post<ICDCFetchResponse>("fb_icdc_fetch", form);
141
- }
142
-
143
- private async post<T>(endpoint: string, body: URLSearchParams): Promise<T> {
144
- const url = `${this.baseUrl}/${endpoint}`;
145
- const response = await fetch(url, {
146
- method: "POST",
147
- headers: {
148
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
149
- "Accept-Language": "en-US,en;q=0.9",
150
- "User-Agent": this.userAgent,
151
- "Origin": this.origin,
152
- "Referer": this.referer,
153
- "Cookie": this.cookies,
154
- "Sec-Fetch-Dest": "empty",
155
- "Sec-Fetch-Mode": "cors",
156
- "Sec-Fetch-Site": "cross-site",
157
- },
158
- body: body.toString(),
159
- });
160
-
161
- if (!response.ok) {
162
- const text = await response.text();
163
- throw new Error(`HTTP error ${response.status}: ${text}`);
164
- }
165
-
166
- return response.json() as Promise<T>;
167
- }
168
-
169
- private calculateIdentitiesHash(identities: Buffer[]): Buffer {
170
- const sorted = [...identities].sort(Buffer.compare);
171
- const hash = createHash("sha256");
172
- for (const identity of sorted) {
173
- hash.update(identity);
174
- }
175
- return hash.digest().subarray(0, 10);
176
- }
177
- }
@@ -1,304 +0,0 @@
1
- import * as https from "node:https";
2
- import * as http from "node:http";
3
- import { str, num } from "../utils/fca-utils.ts";
4
-
5
-
6
- import type { FCAApi } from "fca-unofficial";
7
-
8
- import type {
9
- Attachment,
10
- BaseAttachment,
11
- Thread,
12
- UserInfo,
13
- } from "../models/domain.ts";
14
- import type {
15
- CreateThreadInput,
16
- DeleteThreadInput,
17
- DownloadMediaInput,
18
- GetUserInfoInput,
19
- MarkReadInput,
20
- MuteThreadInput,
21
- RenameThreadInput,
22
- SearchUsersInput,
23
- SendMediaInput,
24
- SendStickerInput,
25
- SetGroupPhotoInput,
26
- } from "../models/messaging.ts";
27
- import type { MediaUploadResult } from "../models/media.ts";
28
- import { FacebookGatewayService } from "./facebook-gateway.service.ts";
29
-
30
- export class MediaService {
31
- public constructor(private readonly gateway: FacebookGatewayService) { }
32
-
33
- // Media send
34
-
35
- public async sendImage(
36
- api: FCAApi,
37
- input: SendMediaInput,
38
- ): Promise<Record<string, unknown>> {
39
- return this.gateway.sendAttachmentMessage(api, {
40
- threadId: input.threadId,
41
- data: input.data,
42
- fileName: input.fileName,
43
- caption: input.caption,
44
- replyToMessageId: input.replyToMessageId,
45
- });
46
- }
47
-
48
- public async sendVideo(
49
- api: FCAApi,
50
- input: SendMediaInput,
51
- ): Promise<Record<string, unknown>> {
52
- return this.gateway.sendAttachmentMessage(api, {
53
- threadId: input.threadId,
54
- data: input.data,
55
- fileName: input.fileName,
56
- caption: input.caption,
57
- replyToMessageId: input.replyToMessageId,
58
- });
59
- }
60
-
61
- public async sendAudio(
62
- api: FCAApi,
63
- input: SendMediaInput,
64
- ): Promise<Record<string, unknown>> {
65
- return this.gateway.sendAttachmentMessage(api, {
66
- threadId: input.threadId,
67
- data: input.data,
68
- fileName: input.fileName,
69
- caption: input.caption,
70
- replyToMessageId: input.replyToMessageId,
71
- });
72
- }
73
-
74
- public async sendFile(
75
- api: FCAApi,
76
- input: SendMediaInput,
77
- ): Promise<Record<string, unknown>> {
78
- return this.gateway.sendAttachmentMessage(api, {
79
- threadId: input.threadId,
80
- data: input.data,
81
- fileName: input.fileName,
82
- caption: input.caption,
83
- replyToMessageId: input.replyToMessageId,
84
- });
85
- }
86
-
87
- public async sendSticker(
88
- api: FCAApi,
89
- input: SendStickerInput,
90
- ): Promise<Record<string, unknown>> {
91
- return this.gateway.sendStickerMessage(api, {
92
- threadId: input.threadId,
93
- stickerId: input.stickerId,
94
- replyToMessageId: input.replyToMessageId,
95
- });
96
- }
97
-
98
- // Media download
99
-
100
- /**
101
- * Downloads raw bytes from a Facebook CDN URL.
102
- * Uses Node's built-in https/http since fca-unofficial does not expose a
103
- * dedicated download API at the JS level (unlike the Go messagix client).
104
- */
105
- public async downloadMedia(input: DownloadMediaInput): Promise<Buffer> {
106
- return downloadUrl(input.url);
107
- }
108
-
109
- // Attachment normalisation helper (used by client controller)
110
-
111
- public normalizeAttachment(item: unknown): Attachment | null {
112
- if (typeof item !== "object" || item === null) {
113
- return null;
114
- }
115
- const att = item as Record<string, unknown>;
116
- const type = typeof att.type === "string" ? att.type : "";
117
- if (!type) return null;
118
-
119
- const base: BaseAttachment = {
120
- url: str(att.url ?? att.previewUrl ?? att.largePreviewUrl),
121
- fileName: str(att.filename ?? att.name),
122
- mimeType: str(att.mimeType),
123
- fileSize: num(att.fileSize),
124
- mediaKey: str(att.mediaKey) || undefined,
125
- mediaSha256: str(att.mediaSha256) || undefined,
126
- mediaEncSha256: str(att.mediaEncSha256) || undefined,
127
- directPath: str(att.directPath) || undefined,
128
- };
129
-
130
- switch (type) {
131
- case "image":
132
- case "gif":
133
- case "photo":
134
- return {
135
- ...base,
136
- type,
137
- width: num(att.width),
138
- height: num(att.height),
139
- previewUrl: str(att.thumbnailUrl ?? att.previewUrl),
140
- };
141
- case "video":
142
- return {
143
- ...base,
144
- type,
145
- width: num(att.width),
146
- height: num(att.height),
147
- duration: num(att.duration ?? att.durationMs),
148
- previewUrl: str(att.thumbnailUrl ?? att.previewUrl),
149
- };
150
- case "audio":
151
- case "voice":
152
- return {
153
- ...base,
154
- type,
155
- duration: num(att.duration ?? att.durationMs),
156
- };
157
- case "sticker":
158
- return {
159
- ...base,
160
- type,
161
- stickerID: num(att.stickerID),
162
- };
163
- case "location":
164
- return {
165
- ...base,
166
- type,
167
- latitude: num(att.latitude),
168
- longitude: num(att.longitude),
169
- };
170
- case "link":
171
- return {
172
- ...base,
173
- type,
174
- description: str(att.description),
175
- sourceText: str(att.source),
176
- previewUrl: str(att.thumbnailUrl ?? att.previewUrl),
177
- };
178
- default:
179
- return {
180
- ...base,
181
- type: "file",
182
- };
183
- }
184
- }
185
-
186
- // Thread / group management (mirrors bridge-go media.go)
187
-
188
- public async muteThread(api: FCAApi, input: MuteThreadInput): Promise<void> {
189
- if (!api.muteThread) {
190
- throw new Error("muteThread not available in fca-unofficial");
191
- }
192
- await Promise.resolve(api.muteThread(input.threadId, input.muteSeconds));
193
- }
194
-
195
- public async renameThread(
196
- api: FCAApi,
197
- input: RenameThreadInput,
198
- ): Promise<void> {
199
- if (!api.setTitle) {
200
- throw new Error("setTitle not available in fca-unofficial");
201
- }
202
- await Promise.resolve(api.setTitle(input.newName, input.threadId));
203
- }
204
-
205
- public async setGroupPhoto(
206
- api: FCAApi,
207
- input: SetGroupPhotoInput,
208
- ): Promise<void> {
209
- if (!api.changeGroupImage) {
210
- throw new Error("changeGroupImage not available in fca-unofficial");
211
- }
212
- // fca-unofficial accepts a Readable stream or Buffer for the image
213
- await Promise.resolve(api.changeGroupImage(input.data, input.threadId));
214
- }
215
-
216
- public async deleteThread(
217
- api: FCAApi,
218
- input: DeleteThreadInput,
219
- ): Promise<void> {
220
- if (!api.deleteThread) {
221
- throw new Error("deleteThread not available in fca-unofficial");
222
- }
223
- await Promise.resolve(api.deleteThread(input.threadId));
224
- }
225
-
226
- // User / search (mirrors bridge-go media.go SearchUsers / GetUserInfo / CreateThread)
227
-
228
- public async searchUsers(
229
- api: FCAApi,
230
- input: SearchUsersInput,
231
- ): Promise<UserInfo[]> {
232
- if (!api.searchUsers) {
233
- throw new Error("searchUsers not available in fca-unofficial");
234
- }
235
- const raw = await Promise.resolve(api.searchUsers(input.query));
236
- if (!raw) return [];
237
- // fca-unofficial returns an array of {[id]: FriendInfo} objects
238
- const entries = Array.isArray(raw)
239
- ? (raw as Record<string, unknown>[]).flatMap(item => Object.entries(item))
240
- : Object.entries(raw as Record<string, unknown>);
241
- return entries.map(([id, infoRaw]) => {
242
- const info = infoRaw as { name?: string; firstName?: string; vanity?: string; thumbSrc?: string; gender?: number };
243
- return {
244
- id,
245
- name: info.name ?? "",
246
- firstName: info.firstName,
247
- username: info.vanity,
248
- profilePictureUrl: info.thumbSrc,
249
- gender: info.gender,
250
- };
251
- });
252
- }
253
-
254
- public async getUserInfo(
255
- api: FCAApi,
256
- input: GetUserInfoInput,
257
- ): Promise<UserInfo | null> {
258
- if (!api.getUserInfo) {
259
- throw new Error("getUserInfo not available in fca-unofficial");
260
- }
261
- const raw = await Promise.resolve(api.getUserInfo(input.userId));
262
- if (!raw) return null;
263
- const info = raw[input.userId];
264
- if (!info) return null;
265
- return {
266
- id: input.userId,
267
- name: info.name ?? "",
268
- firstName: info.firstName,
269
- username: info.vanity,
270
- profilePictureUrl: info.thumbSrc,
271
- gender: info.gender,
272
- };
273
- }
274
-
275
- public async createThread(
276
- api: FCAApi,
277
- input: CreateThreadInput,
278
- ): Promise<Thread> {
279
- // fca-unofficial createNewGroup with a single user creates a DM-like thread
280
- if (!api.createNewGroup) {
281
- // Fallback: treat the userId as the threadId (1:1 DM convention)
282
- return { id: input.userId, type: 1, name: "", lastActivityTimestampMs: Date.now() };
283
- }
284
- const result = await Promise.resolve(
285
- api.createNewGroup([input.userId], "", undefined),
286
- );
287
- const threadId = str((result as Record<string, unknown>)?.threadID ?? input.userId) || input.userId;
288
- return { id: threadId, type: 1, name: "", lastActivityTimestampMs: Date.now() };
289
- }
290
- }
291
-
292
- // Internal helpers
293
-
294
- function downloadUrl(url: string): Promise<Buffer> {
295
- return new Promise((resolve, reject) => {
296
- const mod = url.startsWith("https") ? https : http;
297
- mod.get(url, (res: any) => {
298
- const chunks: Buffer[] = [];
299
- res.on("data", (chunk: Buffer) => chunks.push(chunk));
300
- res.on("end", () => resolve(Buffer.concat(chunks)));
301
- res.on("error", reject);
302
- }).on("error", reject);
303
- });
304
- }
@@ -1,28 +0,0 @@
1
- import type { FCAApi } from "fca-unofficial";
2
-
3
- import type { MarkReadInput, SendMessageInput, SendReactionInput, TypingInput } from "../models/messaging.ts";
4
- import { FacebookGatewayService } from "./facebook-gateway.service.ts";
5
-
6
- export class MessagingService {
7
- public constructor(private readonly gateway: FacebookGatewayService) {}
8
-
9
- public async sendText(api: FCAApi, input: SendMessageInput): Promise<Record<string, unknown>> {
10
- return this.gateway.sendMessage(api, input.threadId, input.text, input.replyToMessageId);
11
- }
12
-
13
- public async react(api: FCAApi, input: SendReactionInput): Promise<void> {
14
- await this.gateway.sendReaction(api, input.messageId, input.reaction);
15
- }
16
-
17
- public async unsend(api: FCAApi, messageId: string): Promise<void> {
18
- await this.gateway.unsendMessage(api, messageId);
19
- }
20
-
21
- public async sendTyping(api: FCAApi, input: TypingInput): Promise<void> {
22
- await this.gateway.sendTyping(api, input.threadId, input.isTyping);
23
- }
24
-
25
- public async markAsRead(api: FCAApi, input: MarkReadInput): Promise<void> {
26
- await this.gateway.markAsRead(api, input.threadId);
27
- }
28
- }
@@ -1,199 +0,0 @@
1
- import type { FCAApi, HistoryMessage, ThreadListItem } from "fca-unofficial";
2
-
3
- import type {
4
- Attachment,
5
- MessengerMessage,
6
- Thread,
7
- UserInfo,
8
- } from "../models/domain.ts";
9
- import type {
10
- AddGroupMemberInput,
11
- ChangeAdminStatusInput,
12
- CreatePollInput,
13
- EditMessageInput,
14
- EditMessageResult,
15
- ForwardAttachmentInput,
16
- GetThreadHistoryInput,
17
- GetThreadListInput,
18
- RemoveGroupMemberInput,
19
- ThreadDetails,
20
- } from "../models/thread.ts";
21
- import type { MediaService } from "./media.service.ts";
22
-
23
-
24
- // ThreadService
25
-
26
- export class ThreadService {
27
- public constructor(private readonly mediaService: MediaService) { }
28
-
29
- // Thread list
30
-
31
- public async getThreadList(
32
- api: FCAApi,
33
- input: GetThreadListInput,
34
- ): Promise<ThreadDetails[]> {
35
- if (!api.getThreadList) {
36
- throw new Error("getThreadList not available in fca-unofficial");
37
- }
38
- const tags = input.folder ? [input.folder] : [""];
39
- const raw = await Promise.resolve(
40
- api.getThreadList(input.limit, input.beforeTimestamp ?? null, tags),
41
- );
42
- if (!raw) return [];
43
- return (raw as ThreadListItem[]).map(t => this.mapThread(t));
44
- }
45
-
46
- // Thread history
47
-
48
- public async getThreadHistory(
49
- api: FCAApi,
50
- input: GetThreadHistoryInput,
51
- ): Promise<MessengerMessage[]> {
52
- if (!api.getThreadHistory) {
53
- throw new Error("getThreadHistory not available in fca-unofficial");
54
- }
55
- const raw = await Promise.resolve(
56
- api.getThreadHistory(
57
- input.threadId,
58
- input.amount,
59
- input.beforeTimestamp,
60
- ),
61
- );
62
- if (!raw) return [];
63
- return (raw as HistoryMessage[]).map(m => this.mapHistoryMessage(m));
64
- }
65
-
66
- // Forward attachment
67
-
68
- public async forwardAttachment(
69
- api: FCAApi,
70
- input: ForwardAttachmentInput,
71
- ): Promise<void> {
72
- if (!api.forwardAttachment) {
73
- throw new Error("forwardAttachment not available in fca-unofficial");
74
- }
75
- await Promise.resolve(
76
- api.forwardAttachment(input.attachmentId, input.threadIds),
77
- );
78
- }
79
-
80
- // Poll
81
-
82
- public async createPoll(api: FCAApi, input: CreatePollInput): Promise<void> {
83
- if (!api.createPoll) {
84
- throw new Error("createPoll not available in fca-unofficial");
85
- }
86
- await Promise.resolve(
87
- api.createPoll(input.title, input.threadId, input.options ?? {}),
88
- );
89
- }
90
-
91
- // Edit message (non-E2EE; uses MQTT internally)
92
-
93
- public async editMessage(
94
- api: FCAApi,
95
- input: EditMessageInput,
96
- ): Promise<EditMessageResult> {
97
- if (!api.editMessage) {
98
- throw new Error("editMessage not available in fca-unofficial");
99
- }
100
- const res = await Promise.resolve(
101
- api.editMessage(input.newText, input.messageId),
102
- );
103
- const r = (res ?? {}) as Record<string, unknown>;
104
- return {
105
- messageId: typeof r["messageID"] === "string" ? r["messageID"] : input.messageId,
106
- newText: typeof r["body"] === "string" ? r["body"] : input.newText,
107
- };
108
- }
109
-
110
- // Group member management
111
-
112
- public async addGroupMember(
113
- api: FCAApi,
114
- input: AddGroupMemberInput,
115
- ): Promise<void> {
116
- if (!api.addUserToGroup) {
117
- throw new Error("addUserToGroup not available in fca-unofficial");
118
- }
119
- const ids = input.userIds.length === 1 ? (input.userIds[0] ?? "") : input.userIds;
120
- await Promise.resolve(api.addUserToGroup(ids, input.threadId));
121
- }
122
-
123
- public async removeGroupMember(
124
- api: FCAApi,
125
- input: RemoveGroupMemberInput,
126
- ): Promise<void> {
127
- if (!api.removeUserFromGroup) {
128
- throw new Error("removeUserFromGroup not available in fca-unofficial");
129
- }
130
- await Promise.resolve(api.removeUserFromGroup(input.userId, input.threadId));
131
- }
132
-
133
- public async changeAdminStatus(
134
- api: FCAApi,
135
- input: ChangeAdminStatusInput,
136
- ): Promise<void> {
137
- if (!api.changeAdminStatus) {
138
- throw new Error("changeAdminStatus not available in fca-unofficial");
139
- }
140
- await Promise.resolve(
141
- api.changeAdminStatus(input.threadId, input.userId, input.isAdmin),
142
- );
143
- }
144
-
145
- // Friends list
146
-
147
- public async getFriendsList(api: FCAApi): Promise<UserInfo[]> {
148
- if (!api.getFriendsList) {
149
- throw new Error("getFriendsList not available in fca-unofficial");
150
- }
151
- const raw = await Promise.resolve(api.getFriendsList());
152
- if (!raw) return [];
153
- return Object.entries(raw).map(([id, info]) => ({
154
- id,
155
- name: info.name ?? "",
156
- firstName: info.firstName,
157
- username: info.vanity,
158
- profilePictureUrl: info.thumbSrc,
159
- gender: info.gender,
160
- }));
161
- }
162
-
163
- // Private helpers
164
-
165
- private mapThread(t: ThreadListItem): ThreadDetails {
166
- return {
167
- id: t.threadID,
168
- type: t.threadType,
169
- name: t.name ?? "",
170
- lastActivityTimestampMs: t.timestamp ? Number(t.timestamp) : 0,
171
- snippet: t.snippet,
172
- unreadCount: t.unreadCount,
173
- messageCount: t.messageCount,
174
- emoji: t.emoji,
175
- muteUntil: t.muteUntil,
176
- participantIds: t.participantIDs,
177
- adminIds: t.adminIDs,
178
- isArchived: t.isArchived,
179
- folder: t.folder,
180
- };
181
- }
182
-
183
- private mapHistoryMessage(m: HistoryMessage): MessengerMessage {
184
- const attachments = Array.isArray(m.attachments) && m.attachments.length > 0
185
- ? m.attachments
186
- .map(a => this.mediaService.normalizeAttachment(a))
187
- .filter((a): a is Attachment => a !== null)
188
- : undefined;
189
-
190
- return {
191
- id: m.messageID,
192
- threadId: m.threadID,
193
- senderId: m.senderID,
194
- text: m.body ?? "",
195
- timestampMs: m.timestamp ? Number(m.timestamp) : 0,
196
- attachments: attachments && attachments.length > 0 ? attachments : undefined,
197
- };
198
- }
199
- }