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,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
- }