fb-messenger-e2ee 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +41 -3
- package/README.md +27 -2
- package/dist/index.cjs +9017 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +910 -0
- package/dist/index.d.ts +910 -0
- package/dist/index.js +9049 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -4
- package/bun.lock +0 -1014
- package/examples/basic-usage.ts +0 -52
- package/jest.config.ts +0 -18
- package/src/config/env.ts +0 -15
- package/src/controllers/client.controller.ts +0 -805
- package/src/controllers/dgw-handler.ts +0 -171
- package/src/controllers/e2ee-handler.ts +0 -832
- package/src/controllers/event-mapper.ts +0 -327
- package/src/core/client.ts +0 -151
- package/src/e2ee/application/e2ee-client.ts +0 -376
- package/src/e2ee/application/fanout-planner.ts +0 -79
- package/src/e2ee/application/outbound-message-cache.ts +0 -58
- package/src/e2ee/application/prekey-maintenance.ts +0 -55
- package/src/e2ee/application/retry-manager.ts +0 -156
- package/src/e2ee/facebook/facebook-protocol-utils.ts +0 -230
- package/src/e2ee/facebook/icdc-payload.ts +0 -31
- package/src/e2ee/index.ts +0 -76
- package/src/e2ee/internal.ts +0 -27
- package/src/e2ee/media/media-crypto.ts +0 -147
- package/src/e2ee/media/media-upload.ts +0 -90
- package/src/e2ee/message/builders/client-payload.ts +0 -54
- package/src/e2ee/message/builders/consumer-application.ts +0 -362
- package/src/e2ee/message/builders/message-application.ts +0 -64
- package/src/e2ee/message/builders/message-transport.ts +0 -84
- package/src/e2ee/message/codecs/protobuf-codecs.ts +0 -101
- package/src/e2ee/message/constants.ts +0 -4
- package/src/e2ee/message/message-builder.ts +0 -15
- package/src/e2ee/signal/prekey-manager.ts +0 -140
- package/src/e2ee/signal/signal-manager.ts +0 -365
- package/src/e2ee/store/device-json.ts +0 -47
- package/src/e2ee/store/device-repository.ts +0 -12
- package/src/e2ee/store/device-store.ts +0 -538
- package/src/e2ee/transport/binary/decoder.ts +0 -180
- package/src/e2ee/transport/binary/encoder.ts +0 -143
- package/src/e2ee/transport/binary/stanzas.ts +0 -97
- package/src/e2ee/transport/binary/tokens.ts +0 -64
- package/src/e2ee/transport/binary/wa-binary.ts +0 -8
- package/src/e2ee/transport/dgw/dgw-socket.ts +0 -345
- package/src/e2ee/transport/noise/noise-handshake.ts +0 -417
- package/src/e2ee/transport/noise/noise-socket.ts +0 -230
- package/src/index.ts +0 -34
- package/src/models/client.ts +0 -26
- package/src/models/config.ts +0 -14
- package/src/models/domain.ts +0 -299
- package/src/models/e2ee.ts +0 -295
- package/src/models/media.ts +0 -41
- package/src/models/messaging.ts +0 -88
- package/src/models/thread.ts +0 -69
- package/src/repositories/session.repository.ts +0 -20
- package/src/services/auth.service.ts +0 -55
- package/src/services/e2ee.service.ts +0 -174
- package/src/services/facebook-gateway.service.ts +0 -245
- package/src/services/icdc.service.ts +0 -177
- package/src/services/media.service.ts +0 -304
- package/src/services/messaging.service.ts +0 -28
- package/src/services/thread.service.ts +0 -199
- package/src/types/advanced-types.ts +0 -61
- package/src/types/fca-unofficial.d.ts +0 -212
- package/src/types/protocol-types.ts +0 -43
- package/src/utils/fca-utils.ts +0 -30
- package/src/utils/logger.ts +0 -24
- package/src/utils/mime.ts +0 -51
- package/tests/.env.example +0 -87
- package/tests/data/1x1.png +0 -0
- package/tests/data/example.txt +0 -1
- package/tests/data/file_example.mp3 +0 -0
- package/tests/data/file_example.mp4 +0 -0
- package/tests/integration.test.ts +0 -498
- package/tests/script/echo-e2ee.ts +0 -174
- package/tests/script/send-e2ee-media.ts +0 -227
- package/tests/script/send-e2ee-reaction.ts +0 -108
- package/tests/script/send-e2ee-unsend.ts +0 -115
- package/tests/script/send-typing.ts +0 -107
- package/tests/script/test-group-send.ts +0 -105
- package/tests/setup.ts +0 -3
- package/tests/types.test.ts +0 -57
- package/tests/unit/controllers/dgw-handler.test.ts +0 -60
- package/tests/unit/controllers/e2ee-handler.test.ts +0 -293
- package/tests/unit/controllers/event-mapper.test.ts +0 -252
- package/tests/unit/e2ee/application-helpers.test.ts +0 -418
- package/tests/unit/e2ee/device-store.test.ts +0 -126
- package/tests/unit/e2ee/facebook-protocol-utils.test.ts +0 -134
- package/tests/unit/e2ee/media-crypto.test.ts +0 -55
- package/tests/unit/e2ee/media-upload.test.ts +0 -60
- package/tests/unit/e2ee/message-builder.test.ts +0 -42
- package/tests/unit/e2ee/message-builders.test.ts +0 -230
- package/tests/unit/e2ee/noise-handshake.test.ts +0 -260
- package/tests/unit/e2ee/prekey-manager.test.ts +0 -55
- package/tests/unit/e2ee/wa-binary.test.ts +0 -127
- package/tests/unit/services/e2ee.service.test.ts +0 -101
- package/tests/unit/services/facebook-gateway.service.test.ts +0 -138
- package/tests/unit/services/media.service.test.ts +0 -169
- package/tests/unit/utils/fca-utils.test.ts +0 -48
- package/tsconfig.json +0 -19
- /package/{src/e2ee/message/proto → proto}/ArmadilloApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ArmadilloICDC.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/ConsumerApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageApplication.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/MessageTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WACommon.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/WAMediaTransport.proto +0 -0
- /package/{src/e2ee/message/proto → proto}/proto-writer.ts +0 -0
|
@@ -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
|
-
}
|