@dongdev/fca-unofficial 3.0.25 → 3.0.28

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 (35) hide show
  1. package/.gitattributes +2 -0
  2. package/CHANGELOG.md +196 -190
  3. package/DOCS.md +3 -6
  4. package/Fca_Database/database.sqlite +0 -0
  5. package/LICENSE-MIT +1 -1
  6. package/README.md +1 -1
  7. package/index.d.ts +745 -746
  8. package/module/config.js +29 -33
  9. package/module/login.js +133 -136
  10. package/module/loginHelper.js +1240 -1048
  11. package/module/options.js +44 -45
  12. package/package.json +81 -82
  13. package/src/api/messaging/changeAdminStatus.js +56 -56
  14. package/src/api/messaging/changeGroupImage.js +2 -1
  15. package/src/api/messaging/changeThreadEmoji.js +47 -47
  16. package/src/api/messaging/createPoll.js +25 -25
  17. package/src/api/messaging/deleteMessage.js +110 -30
  18. package/src/api/messaging/forwardAttachment.js +28 -28
  19. package/src/api/messaging/removeUserFromGroup.js +28 -73
  20. package/src/api/messaging/sendMessage.js +15 -17
  21. package/src/api/messaging/sendTypingIndicator.js +23 -23
  22. package/src/api/messaging/setMessageReaction.js +57 -60
  23. package/src/api/messaging/setTitle.js +47 -47
  24. package/src/api/messaging/uploadAttachment.js +471 -73
  25. package/src/api/socket/core/connectMqtt.js +250 -250
  26. package/src/api/socket/core/emitAuth.js +1 -1
  27. package/src/api/socket/core/getSeqID.js +322 -40
  28. package/src/api/socket/core/parseDelta.js +368 -377
  29. package/src/api/socket/listenMqtt.js +371 -360
  30. package/src/utils/client.js +2 -312
  31. package/src/utils/cookies.js +68 -0
  32. package/src/utils/format.js +117 -90
  33. package/src/utils/loginParser.js +347 -0
  34. package/src/utils/messageFormat.js +1173 -0
  35. package/src/api/socket/core/markDelivery.js +0 -12
@@ -1,40 +1,322 @@
1
- "use strict";
2
- const { getType } = require("../../../utils/format");
3
- const { parseAndCheckLogin } = require("../../../utils/client");
4
- module.exports = function createGetSeqID(deps) {
5
- const { listenMqtt, logger, emitAuth } = deps;
6
-
7
- return function getSeqID(defaultFuncs, api, ctx, globalCallback, form) {
8
- ctx.t_mqttCalled = false;
9
- return defaultFuncs
10
- .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
11
- .then(parseAndCheckLogin(ctx, defaultFuncs))
12
- .then(resData => {
13
- if (getType(resData) !== "Array") throw { error: "Not logged in" };
14
- if (!Array.isArray(resData) || !resData.length) return;
15
- const lastRes = resData[resData.length - 1];
16
- if (lastRes && lastRes.successful_results === 0) return;
17
-
18
- const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
19
- if (syncSeqId) {
20
- ctx.lastSeqId = syncSeqId;
21
- logger("mqtt getSeqID ok -> listenMqtt()", "info");
22
- listenMqtt(defaultFuncs, api, ctx, globalCallback);
23
- } else {
24
- throw { error: "getSeqId: no sync_sequence_id found." };
25
- }
26
- })
27
- .catch(err => {
28
- const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
29
- const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
30
- if (/Not logged in/i.test(msg)) {
31
- return emitAuth(ctx, api, globalCallback, "not_logged_in", msg);
32
- }
33
- if (/blocked the login/i.test(msg)) {
34
- return emitAuth(ctx, api, globalCallback, "login_blocked", msg);
35
- }
36
- logger(`getSeqID error: ${msg}`, "error");
37
- return emitAuth(ctx, api, globalCallback, "auth_error", msg);
38
- });
39
- };
40
- };
1
+ "use strict";
2
+ const { getType } = require("../../../utils/format");
3
+ const { parseAndCheckLogin, saveCookies } = require("../../../utils/client");
4
+ const path = require("path");
5
+
6
+ // Load config for auto-login credentials
7
+ function getConfig() {
8
+ try {
9
+ const configPath = path.join(process.cwd(), "fca-config.json");
10
+ const fs = require("fs");
11
+ if (fs.existsSync(configPath)) {
12
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
13
+ }
14
+ } catch { }
15
+ return {};
16
+ }
17
+
18
+ // Parse cookie string to array format
19
+ function parseCookieString(cookieStr) {
20
+ if (!cookieStr || typeof cookieStr !== "string") return [];
21
+ const cookies = [];
22
+ const pairs = cookieStr.split(";").map(p => p.trim()).filter(Boolean);
23
+ for (const pair of pairs) {
24
+ const eq = pair.indexOf("=");
25
+ if (eq > 0) {
26
+ const key = pair.slice(0, eq).trim();
27
+ const value = pair.slice(eq + 1).trim();
28
+ if (key && value) {
29
+ cookies.push({
30
+ key,
31
+ value,
32
+ domain: ".facebook.com",
33
+ path: "/"
34
+ });
35
+ }
36
+ }
37
+ }
38
+ return cookies;
39
+ }
40
+
41
+ // Try to auto-login using API and refresh web session
42
+ async function tryAutoLogin(logger, config, ctx, defaultFuncs) {
43
+ const email = config.credentials?.email || config.email;
44
+ const password = config.credentials?.password || config.password;
45
+ const twofactor = config.credentials?.twofactor || config.twofactor || null;
46
+
47
+ if (config.autoLogin === false || !email || !password) {
48
+ return null;
49
+ }
50
+
51
+ logger("getSeqID: attempting auto re-login via API...", "warn");
52
+
53
+ try {
54
+ const loginHelper = require("../../../../module/loginHelper");
55
+ const result = await loginHelper.tokensViaAPI(
56
+ email,
57
+ password,
58
+ twofactor,
59
+ config.apiServer || null
60
+ );
61
+
62
+ if (result && result.status) {
63
+ // Handle cookies - can be array, cookie string header, or both
64
+ // Import normalizeCookieHeaderString from loginHelper
65
+ const loginHelper = require("../../../../module/loginHelper");
66
+ const normalizeCookieHeaderString = loginHelper.normalizeCookieHeaderString;
67
+
68
+ let cookiePairs = [];
69
+
70
+ // If cookies is a string (cookie header format), parse it
71
+ if (typeof result.cookies === "string") {
72
+ cookiePairs = normalizeCookieHeaderString(result.cookies);
73
+ }
74
+ // If cookies is an array, convert to pairs
75
+ else if (Array.isArray(result.cookies)) {
76
+ cookiePairs = result.cookies.map(c => {
77
+ if (typeof c === "string") {
78
+ // Already in "key=value" format
79
+ return c;
80
+ } else if (c && typeof c === "object") {
81
+ // Object format {key, value} or {name, value}
82
+ return `${c.key || c.name}=${c.value}`;
83
+ }
84
+ return null;
85
+ }).filter(Boolean);
86
+ }
87
+
88
+ // Also check for cookie field (alternative field name)
89
+ if (cookiePairs.length === 0 && result.cookie) {
90
+ if (typeof result.cookie === "string") {
91
+ cookiePairs = normalizeCookieHeaderString(result.cookie);
92
+ } else if (Array.isArray(result.cookie)) {
93
+ cookiePairs = result.cookie.map(c => {
94
+ if (typeof c === "string") return c;
95
+ if (c && typeof c === "object") return `${c.key || c.name}=${c.value}`;
96
+ return null;
97
+ }).filter(Boolean);
98
+ }
99
+ }
100
+
101
+ if (cookiePairs.length > 0 || result.uid) {
102
+ logger(`getSeqID: auto re-login successful! UID: ${result.uid}, Cookies: ${cookiePairs.length}`, "info");
103
+
104
+ // Apply cookies to ctx.jar using setJarFromPairs approach
105
+ if (ctx.jar && cookiePairs.length > 0) {
106
+ const expires = new Date(Date.now() + 31536e6).toUTCString();
107
+ for (const kv of cookiePairs) {
108
+ const cookieStr = `${kv}; expires=${expires}; domain=.facebook.com; path=/;`;
109
+ try {
110
+ if (typeof ctx.jar.setCookieSync === "function") {
111
+ ctx.jar.setCookieSync(cookieStr, "https://www.facebook.com");
112
+ } else if (typeof ctx.jar.setCookie === "function") {
113
+ await ctx.jar.setCookie(cookieStr, "https://www.facebook.com");
114
+ }
115
+ } catch (err) {
116
+ logger(`getSeqID: Failed to set cookie ${kv.substring(0, 50)}: ${err && err.message ? err.message : String(err)}`, "warn");
117
+ }
118
+ }
119
+ logger(`getSeqID: applied ${cookiePairs.length} API cookies to jar`, "info");
120
+ }
121
+
122
+ // Now refresh web session to get proper web cookies
123
+ logger("getSeqID: refreshing web session after API login...", "info");
124
+ try {
125
+ const { get, jar: globalJar } = require("../../../utils/request");
126
+ const { saveCookies: saveWebCookies } = require("../../../utils/client");
127
+
128
+ // Apply cookies to global jar as well using cookie pairs
129
+ const expires = new Date(Date.now() + 31536e6).toUTCString();
130
+ for (const kv of cookiePairs) {
131
+ const cookieStr = `${kv}; expires=${expires}; domain=.facebook.com; path=/;`;
132
+ try {
133
+ if (typeof globalJar.setCookieSync === "function") {
134
+ globalJar.setCookieSync(cookieStr, "https://www.facebook.com");
135
+ } else if (typeof globalJar.setCookie === "function") {
136
+ await globalJar.setCookie(cookieStr, "https://www.facebook.com");
137
+ }
138
+ } catch (err) {
139
+ logger(`getSeqID: Failed to set cookie in global jar ${kv.substring(0, 50)}: ${err && err.message ? err.message : String(err)}`, "warn");
140
+ }
141
+ }
142
+
143
+ // Fetch Facebook to refresh and get web session cookies
144
+ // Do this multiple times to ensure session is fully established
145
+ // Try both m.facebook.com and www.facebook.com
146
+ let webResponse = null;
147
+ let htmlContent = "";
148
+ const htmlUID = body => {
149
+ const s = typeof body === "string" ? body : String(body ?? "");
150
+ return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
151
+ };
152
+ const isValidUID = uid => uid && uid !== "0" && /^\d+$/.test(uid) && parseInt(uid, 10) > 0;
153
+ const urlsToTry = ["https://m.facebook.com/", "https://www.facebook.com/"];
154
+
155
+ // Try refreshing up to 3 times to get valid session
156
+ for (let attempt = 0; attempt < 3; attempt++) {
157
+ try {
158
+ // Try m.facebook.com first (mobile version often works better for API login)
159
+ const urlToUse = attempt === 0 ? urlsToTry[0] : urlsToTry[attempt % urlsToTry.length];
160
+ logger(`getSeqID: Refreshing ${urlToUse} (attempt ${attempt + 1}/3)...`, "info");
161
+
162
+ webResponse = await get(urlToUse, ctx.jar, null, ctx.globalOptions, ctx);
163
+ if (webResponse && webResponse.data) {
164
+ await saveWebCookies(ctx.jar)(webResponse);
165
+ htmlContent = typeof webResponse.data === "string" ? webResponse.data : String(webResponse.data || "");
166
+
167
+ // Check if HTML contains valid USER_ID
168
+ const htmlUserID = htmlUID(htmlContent);
169
+ if (isValidUID(htmlUserID)) {
170
+ logger(`getSeqID: Found valid USER_ID in HTML from ${urlToUse}: ${htmlUserID}`, "info");
171
+ break;
172
+ } else if (attempt < 2) {
173
+ logger(`getSeqID: No valid USER_ID in HTML from ${urlToUse} (attempt ${attempt + 1}/3), retrying...`, "warn");
174
+ await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
175
+ }
176
+ }
177
+ } catch (refreshErr) {
178
+ logger(`getSeqID: Error refreshing session (attempt ${attempt + 1}/3): ${refreshErr && refreshErr.message ? refreshErr.message : String(refreshErr)}`, "warn");
179
+ if (attempt < 2) {
180
+ await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
181
+ }
182
+ }
183
+ }
184
+
185
+ if (webResponse && webResponse.data) {
186
+ // Get updated cookies from jar
187
+ const updatedCookies = await ctx.jar.getCookies("https://www.facebook.com");
188
+ logger(`getSeqID: refreshed session, now have ${updatedCookies.length} web cookies`, "info");
189
+
190
+ // Check HTML for USER_ID
191
+ const htmlUserID = htmlUID(htmlContent);
192
+ if (!isValidUID(htmlUserID)) {
193
+ logger("getSeqID: WARNING - HTML does not show valid USER_ID after refresh. Session may not be fully established.", "warn");
194
+ }
195
+
196
+ // Update ctx state
197
+ if (ctx) {
198
+ ctx.loggedIn = true;
199
+ // Use USER_ID from HTML if available, otherwise from API response
200
+ if (isValidUID(htmlUserID)) {
201
+ ctx.userID = htmlUserID;
202
+ logger(`getSeqID: Updated ctx.userID from HTML: ${htmlUserID}`, "info");
203
+ } else if (result.uid && isValidUID(result.uid)) {
204
+ ctx.userID = result.uid;
205
+ logger(`getSeqID: Updated ctx.userID from API: ${result.uid}`, "info");
206
+ }
207
+ }
208
+ } else {
209
+ logger("getSeqID: Failed to refresh web session after API login", "error");
210
+ }
211
+ } catch (refreshErr) {
212
+ logger(`getSeqID: web session refresh failed - ${refreshErr && refreshErr.message ? refreshErr.message : String(refreshErr)}`, "warn");
213
+ }
214
+
215
+ return { ...result, cookies };
216
+ }
217
+ }
218
+
219
+ logger(`getSeqID: auto re-login failed - ${result && result.message ? result.message : "unknown error"}`, "error");
220
+ } catch (loginErr) {
221
+ logger(`getSeqID: auto re-login error - ${loginErr && loginErr.message ? loginErr.message : String(loginErr)}`, "error");
222
+ }
223
+
224
+ return null;
225
+ }
226
+
227
+ module.exports = function createGetSeqID(deps) {
228
+ const { listenMqtt, logger, emitAuth } = deps;
229
+
230
+ return function getSeqID(defaultFuncs, api, ctx, globalCallback, form, retryCount = 0) {
231
+ const MAX_RETRIES = 3;
232
+ const RETRY_DELAY = 2000;
233
+ ctx.t_mqttCalled = false;
234
+
235
+ return defaultFuncs
236
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
237
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
238
+ .then(async resData => {
239
+ // Better error handling: check what we actually got
240
+ if (getType(resData) !== "Array") {
241
+ // Log what we actually received for debugging
242
+ logger(`getSeqID: Unexpected response type: ${getType(resData)}, value: ${JSON.stringify(resData).substring(0, 200)}`, "warn");
243
+
244
+ // If parseAndCheckLogin returned an error object, check if it's an auth error
245
+ if (resData && typeof resData === "object") {
246
+ const errorMsg = resData.error || resData.message || "";
247
+ if (/Not logged in|login|blocked|401|403|checkpoint/i.test(errorMsg)) {
248
+ throw { error: "Not logged in", originalResponse: resData };
249
+ }
250
+ }
251
+
252
+ throw { error: "Not logged in", originalResponse: resData };
253
+ }
254
+ if (!Array.isArray(resData) || !resData.length) return;
255
+ const lastRes = resData[resData.length - 1];
256
+ if (lastRes && lastRes.successful_results === 0) return;
257
+
258
+ const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
259
+ if (syncSeqId) {
260
+ ctx.lastSeqId = syncSeqId;
261
+ logger("mqtt getSeqID ok -> listenMqtt()", "info");
262
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
263
+ } else {
264
+ throw { error: "getSeqId: no sync_sequence_id found." };
265
+ }
266
+ })
267
+ .catch(async err => {
268
+ const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
269
+ const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
270
+
271
+ // Check if this is an auth-related error
272
+ const isAuthError = /Not logged in|no sync_sequence_id found|blocked the login|401|403/i.test(msg);
273
+
274
+ // For auth errors, try retry + auto-login
275
+ if (isAuthError) {
276
+ // Retry logic with increasing delay
277
+ if (retryCount < MAX_RETRIES) {
278
+ const delay = RETRY_DELAY * (retryCount + 1); // Increasing delay: 2s, 4s, 6s
279
+ logger(`getSeqID: retry ${retryCount + 1}/${MAX_RETRIES} after ${delay}ms... (error: ${msg})`, "warn");
280
+ await new Promise(resolve => setTimeout(resolve, delay));
281
+
282
+ // Before retrying, try to refresh the session if this is the first retry
283
+ if (retryCount === 0 && ctx.loggedIn) {
284
+ try {
285
+ logger("getSeqID: refreshing session before retry...", "info");
286
+ const { get } = require("../../../utils/request");
287
+ const { saveCookies } = require("../../../utils/client");
288
+ await get("https://www.facebook.com/", ctx.jar, null, ctx.globalOptions, ctx).then(saveCookies(ctx.jar));
289
+ } catch (refreshErr) {
290
+ logger(`getSeqID: session refresh failed: ${refreshErr && refreshErr.message ? refreshErr.message : String(refreshErr)}`, "warn");
291
+ }
292
+ }
293
+
294
+ return getSeqID(defaultFuncs, api, ctx, globalCallback, form, retryCount + 1);
295
+ }
296
+
297
+ // All retries failed, try auto-login
298
+ logger("getSeqID: all retries failed, attempting auto re-login...", "warn");
299
+ const config = getConfig();
300
+ const loginResult = await tryAutoLogin(logger, config, ctx, defaultFuncs);
301
+
302
+ if (loginResult) {
303
+ // Wait longer after auto-login to ensure session is ready
304
+ logger("getSeqID: retrying with new session...", "info");
305
+ await new Promise(resolve => setTimeout(resolve, 3000)); // Increased delay to 3s
306
+ return getSeqID(defaultFuncs, api, ctx, globalCallback, form, 0);
307
+ }
308
+
309
+ // Determine the auth error type
310
+ if (/blocked/i.test(msg)) {
311
+ return emitAuth(ctx, api, globalCallback, "login_blocked", msg);
312
+ }
313
+ if (/Not logged in/i.test(msg)) {
314
+ return emitAuth(ctx, api, globalCallback, "not_logged_in", msg);
315
+ }
316
+ }
317
+
318
+ logger(`getSeqID error: ${msg}`, "error");
319
+ return emitAuth(ctx, api, globalCallback, "auth_error", msg);
320
+ });
321
+ };
322
+ };