hridoy-ultimate 8.0.2 → 9.0.0

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 (281) hide show
  1. package/CHANGELOG.md +68 -40
  2. package/LICENSE +23 -0
  3. package/MERGE_REPORT.md +54 -0
  4. package/README.md +158 -113
  5. package/THIRD_PARTY_NOTICES.md +36 -0
  6. package/docs/TEST_RESULTS.md +68 -0
  7. package/docs/UPSTREAM_API_DOCS.md +2712 -0
  8. package/func/checkUpdate.js +6 -0
  9. package/func/logAdapter.js +33 -0
  10. package/func/logger.js +48 -0
  11. package/index.d.ts +812 -0
  12. package/index.js +30 -824
  13. package/module/config.js +46 -0
  14. package/module/login.js +136 -0
  15. package/module/loginHelper.js +1413 -0
  16. package/module/options.js +55 -0
  17. package/package.json +75 -22
  18. package/scripts/run-tests.js +19 -0
  19. package/scripts/syntax-check.js +22 -0
  20. package/src/{addExternalModule.js → api/action/addExternalModule.js} +8 -8
  21. package/src/{changeAvatar.js → api/action/changeAvatar.js} +32 -33
  22. package/src/{changeBio.js → api/action/changeBio.js} +19 -22
  23. package/src/api/action/dismissScrapingWarning.js +50 -0
  24. package/src/api/action/enableAutoSaveAppState.js +73 -0
  25. package/src/{getCurrentUserID.js → api/action/getCurrentUserID.js} +1 -1
  26. package/src/api/action/handleFriendRequest.js +57 -0
  27. package/src/api/action/logout.js +95 -0
  28. package/src/api/action/refreshFb_dtsg.js +47 -0
  29. package/src/{setPostReaction.js → api/action/setPostReaction.js} +23 -31
  30. package/src/{unfriend.js → api/action/unfriend.js} +14 -14
  31. package/src/api/http/httpGet.js +46 -0
  32. package/src/api/http/httpPost.js +52 -0
  33. package/src/api/http/postFormData.js +47 -0
  34. package/src/api/messaging/addUserToGroup.js +68 -0
  35. package/src/api/messaging/changeAdminStatus.js +126 -0
  36. package/src/{changeArchivedStatus.js → api/messaging/changeArchivedStatus.js} +15 -17
  37. package/src/{changeBlockedStatus.js → api/messaging/changeBlockedStatus.js} +13 -16
  38. package/src/api/messaging/changeGroupImage.js +91 -0
  39. package/src/api/messaging/changeNickname.js +70 -0
  40. package/src/api/messaging/changeThreadColor.js +79 -0
  41. package/src/api/messaging/changeThreadEmoji.js +111 -0
  42. package/src/{createNewGroup.js → api/messaging/createNewGroup.js} +24 -26
  43. package/src/api/messaging/createPoll.js +46 -0
  44. package/src/api/messaging/createThemeAI.js +98 -0
  45. package/src/api/messaging/deleteMessage.js +136 -0
  46. package/src/{deleteThread.js → api/messaging/deleteThread.js} +14 -16
  47. package/src/api/messaging/editMessage.js +105 -0
  48. package/src/api/messaging/forwardAttachment.js +57 -0
  49. package/src/api/messaging/forwardMessage.js +134 -0
  50. package/src/{getEmojiUrl.js → api/messaging/getEmojiUrl.js} +2 -2
  51. package/src/{getFriendsList.js → api/messaging/getFriendsList.js} +16 -19
  52. package/src/{getMessage.js → api/messaging/getMessage.js} +160 -180
  53. package/src/api/messaging/getThemePictures.js +62 -0
  54. package/src/api/messaging/handleMessageRequest.js +65 -0
  55. package/src/api/messaging/markAsDelivered.js +69 -0
  56. package/src/api/messaging/markAsRead.js +110 -0
  57. package/src/api/messaging/markAsReadAll.js +56 -0
  58. package/src/api/messaging/markAsSeen.js +68 -0
  59. package/src/api/messaging/muteThread.js +50 -0
  60. package/src/api/messaging/pinMessage.js +115 -0
  61. package/src/api/messaging/removeUserFromGroup.js +62 -0
  62. package/src/{resolvePhotoUrl.js → api/messaging/resolvePhotoUrl.js} +11 -15
  63. package/src/api/messaging/scheduler.js +264 -0
  64. package/src/{searchForThread.js → api/messaging/searchForThread.js} +12 -12
  65. package/src/api/messaging/sendBroadcast.js +93 -0
  66. package/src/api/messaging/sendMessage.js +269 -0
  67. package/src/api/messaging/sendTypingIndicator.js +90 -0
  68. package/src/api/messaging/sessionGuard.js +130 -0
  69. package/src/api/messaging/setMessageReaction.js +114 -0
  70. package/src/api/messaging/setTitle.js +124 -0
  71. package/src/api/messaging/shareContact.js +98 -0
  72. package/src/{threadColors.js → api/messaging/threadColors.js} +2 -5
  73. package/src/api/messaging/unsendMessage.js +105 -0
  74. package/src/api/messaging/uploadAttachment.js +493 -0
  75. package/src/api/socket/OldMessage.js +199 -0
  76. package/src/api/socket/core/connectMqtt.js +304 -0
  77. package/src/api/socket/core/emitAuth.js +95 -0
  78. package/src/api/socket/core/getSeqID.js +331 -0
  79. package/src/api/socket/core/getTaskResponseData.js +25 -0
  80. package/src/api/socket/core/lifecycle.js +117 -0
  81. package/src/api/socket/core/parseDelta.js +387 -0
  82. package/src/api/socket/detail/buildStream.js +215 -0
  83. package/src/api/socket/detail/constants.js +28 -0
  84. package/src/api/socket/e2ee/crypto.js +173 -0
  85. package/src/api/socket/e2ee/index.js +977 -0
  86. package/src/api/socket/e2ee/localMediaServer.js +59 -0
  87. package/src/api/socket/e2ee/mediaDecode.js +155 -0
  88. package/src/api/socket/e2ee/native/NOTICE.md +29 -0
  89. package/{lib → src/api/socket/e2ee/native/lib}/index.mjs +109 -66
  90. package/src/api/socket/e2ee/native/nativeMediaBridge.js +273 -0
  91. package/src/api/socket/e2ee/proto/ArmadilloApplication.proto +281 -0
  92. package/src/api/socket/e2ee/proto/ArmadilloICDC.proto +14 -0
  93. package/src/api/socket/e2ee/proto/ConsumerApplication.proto +232 -0
  94. package/src/api/socket/e2ee/proto/MessageApplication.proto +82 -0
  95. package/src/api/socket/e2ee/proto/MessageTransport.proto +77 -0
  96. package/src/api/socket/e2ee/proto/WACommon.proto +66 -0
  97. package/src/api/socket/e2ee/proto/WAMediaTransport.proto +176 -0
  98. package/src/api/socket/e2ee/proto/proto-writer.ts +76 -0
  99. package/src/api/socket/e2ee/protocol.js +196 -0
  100. package/src/api/socket/e2ee/ratchet.js +219 -0
  101. package/src/api/socket/e2ee/store.js +182 -0
  102. package/src/api/socket/e2ee/vendor/fme/dist/index.cjs +6477 -0
  103. package/src/api/socket/e2ee/vendor/fme/proto/ArmadilloApplication.proto +281 -0
  104. package/src/api/socket/e2ee/vendor/fme/proto/ArmadilloICDC.proto +14 -0
  105. package/src/api/socket/e2ee/vendor/fme/proto/ConsumerApplication.proto +232 -0
  106. package/src/api/socket/e2ee/vendor/fme/proto/MessageApplication.proto +82 -0
  107. package/src/api/socket/e2ee/vendor/fme/proto/MessageTransport.proto +77 -0
  108. package/src/api/socket/e2ee/vendor/fme/proto/WACommon.proto +66 -0
  109. package/src/api/socket/e2ee/vendor/fme/proto/WAMediaTransport.proto +176 -0
  110. package/src/api/socket/listenE2EE.js +87 -0
  111. package/src/api/socket/listenMqtt.js +499 -0
  112. package/src/api/socket/middleware/index.js +216 -0
  113. package/src/api/socket/sendMessage.js +331 -0
  114. package/src/api/socket/sendMessageMqtt.js +69 -0
  115. package/src/{getThreadHistory.js → api/threads/getThreadHistory.js} +114 -132
  116. package/src/api/threads/getThreadInfo.js +329 -0
  117. package/src/api/threads/getThreadList.js +293 -0
  118. package/src/api/threads/getThreadPictures.js +78 -0
  119. package/src/{getUserID.js → api/users/getUserID.js} +22 -18
  120. package/src/api/users/getUserInfo.js +402 -0
  121. package/src/api/users/getUserInfoV2.js +134 -0
  122. package/src/app/MessengerBot.js +209 -0
  123. package/src/app/MessengerContext.js +32 -0
  124. package/src/app/createFcaClient.js +136 -0
  125. package/src/app/threadInfoRealtimeSync.js +284 -0
  126. package/src/core/sendReqMqtt.js +96 -0
  127. package/src/database/helpers.js +53 -0
  128. package/src/database/models/index.js +88 -0
  129. package/src/database/models/thread.js +50 -0
  130. package/src/database/models/user.js +46 -0
  131. package/src/database/threadData.js +94 -0
  132. package/src/database/userData.js +98 -0
  133. package/src/remote/remoteClient.js +123 -0
  134. package/src/utils/antiSuspension.js +219 -0
  135. package/src/utils/broadcast.js +51 -0
  136. package/src/utils/client.js +10 -0
  137. package/src/utils/constants.js +23 -0
  138. package/src/utils/cookies.js +85 -0
  139. package/src/utils/fingerprint.js +110 -0
  140. package/src/utils/format/attachment.js +357 -0
  141. package/src/utils/format/cookie.js +9 -0
  142. package/src/utils/format/date.js +50 -0
  143. package/src/utils/format/decode.js +44 -0
  144. package/src/utils/format/delta.js +195 -0
  145. package/src/utils/format/ids.js +64 -0
  146. package/src/utils/format/index.js +64 -0
  147. package/src/utils/format/message.js +88 -0
  148. package/src/utils/format/presence.js +132 -0
  149. package/src/utils/format/readTyp.js +44 -0
  150. package/src/utils/format/thread.js +42 -0
  151. package/src/utils/format/utils.js +141 -0
  152. package/src/utils/headers.js +125 -0
  153. package/src/utils/humanizer.js +256 -0
  154. package/src/utils/loginParser/autoLogin.js +125 -0
  155. package/src/utils/loginParser/extractDtsg.js +44 -0
  156. package/src/utils/loginParser/helpers.js +43 -0
  157. package/src/utils/loginParser/index.js +10 -0
  158. package/src/utils/loginParser/parseAndCheckLogin.js +241 -0
  159. package/src/utils/loginParser/textUtils.js +28 -0
  160. package/src/utils/nexca-logger.js +144 -0
  161. package/src/utils/nexca-utils.js +687 -0
  162. package/src/utils/request/client.js +26 -0
  163. package/src/utils/request/config.js +23 -0
  164. package/src/utils/request/defaults.js +46 -0
  165. package/src/utils/request/helpers.js +46 -0
  166. package/src/utils/request/index.js +17 -0
  167. package/src/utils/request/methods.js +163 -0
  168. package/src/utils/request/proxy.js +21 -0
  169. package/src/utils/request/retry.js +77 -0
  170. package/src/utils/request/sanitize.js +49 -0
  171. package/src/utils/typingIndicator.js +202 -0
  172. package/test/helpers/stub-modules.js +122 -0
  173. package/test/integration/logout.test.js +69 -0
  174. package/test/integration/mqtt-lifecycle.test.js +195 -0
  175. package/test/integration/package.test.js +116 -0
  176. package/test/integration/typing-wiring.test.js +71 -0
  177. package/test/unit/fingerprint.test.js +19 -0
  178. package/test/unit/humanizer.test.js +32 -0
  179. package/test/unit/lifecycle.test.js +92 -0
  180. package/test/unit/parsers.test.js +43 -0
  181. package/test/unit/typing.test.js +136 -0
  182. package/ANALYSIS.md +0 -31
  183. package/AUDIT_REPORT.md +0 -57
  184. package/advancedSystem.js +0 -95
  185. package/checkUpdate.js +0 -47
  186. package/config.json +0 -51
  187. package/e2ee.js +0 -738
  188. package/examples/basic-bot.js +0 -122
  189. package/examples/e2ee-bot.js +0 -141
  190. package/fca-config.example.json +0 -73
  191. package/scripts/check.js +0 -38
  192. package/src/OldMessage.js +0 -357
  193. package/src/addUserToGroup.js +0 -117
  194. package/src/changeAdminStatus.js +0 -105
  195. package/src/changeAvatarV2.js +0 -86
  196. package/src/changeAvt.js +0 -85
  197. package/src/changeBlockedStatusMqtt.js +0 -75
  198. package/src/changeCover.js +0 -73
  199. package/src/changeGroupImage.js +0 -137
  200. package/src/changeName.js +0 -79
  201. package/src/changeNickname.js +0 -61
  202. package/src/changeThreadColor.js +0 -67
  203. package/src/changeThreadEmoji.js +0 -57
  204. package/src/changeUsername.js +0 -59
  205. package/src/createCommentPost.js +0 -232
  206. package/src/createPoll.js +0 -73
  207. package/src/createPost.js +0 -276
  208. package/src/deleteMessage.js +0 -58
  209. package/src/editMessage.js +0 -76
  210. package/src/editMessageOld.js +0 -62
  211. package/src/follow.js +0 -74
  212. package/src/forwardAttachment.js +0 -62
  213. package/src/friendList.js +0 -104
  214. package/src/getAccess.js +0 -112
  215. package/src/getAvatarUser.js +0 -78
  216. package/src/getBotInitialData.js +0 -42
  217. package/src/getCtx.js +0 -5
  218. package/src/getOptions.js +0 -5
  219. package/src/getRegion.js +0 -7
  220. package/src/getThreadHistoryDeprecated.js +0 -71
  221. package/src/getThreadInfo.js +0 -232
  222. package/src/getThreadInfoDeprecated.js +0 -56
  223. package/src/getThreadList.js +0 -213
  224. package/src/getThreadListDeprecated.js +0 -46
  225. package/src/getThreadPictures.js +0 -59
  226. package/src/getThreadTheme.js +0 -81
  227. package/src/getUID.js +0 -119
  228. package/src/getUserInfo.js +0 -66
  229. package/src/handleFriendRequest.js +0 -46
  230. package/src/handleMessageRequest.js +0 -47
  231. package/src/httpGet.js +0 -49
  232. package/src/httpPost.js +0 -48
  233. package/src/httpPostFormData.js +0 -70
  234. package/src/listenMqtt.js +0 -1081
  235. package/src/listenNotification.js +0 -85
  236. package/src/logout.js +0 -18
  237. package/src/markAsDelivered.js +0 -47
  238. package/src/markAsRead.js +0 -72
  239. package/src/markAsReadAll.js +0 -40
  240. package/src/markAsSeen.js +0 -48
  241. package/src/metaTheme.js +0 -189
  242. package/src/muteThread.js +0 -45
  243. package/src/note.js +0 -228
  244. package/src/pinMessage.js +0 -54
  245. package/src/refreshFb_dtsg.js +0 -49
  246. package/src/removeSuspiciousAccount.js +0 -78
  247. package/src/removeUserFromGroup.js +0 -81
  248. package/src/reply.js +0 -442
  249. package/src/searchFriends.js +0 -140
  250. package/src/searchStickers.js +0 -53
  251. package/src/send.js +0 -46
  252. package/src/sendButtons.js +0 -163
  253. package/src/sendComment.js +0 -159
  254. package/src/sendFriendRequest.js +0 -112
  255. package/src/sendMessage.js +0 -441
  256. package/src/sendMessageMqtt.js +0 -323
  257. package/src/sendTypingIndicator.js +0 -68
  258. package/src/sendTypingIndicatorV2.js +0 -22
  259. package/src/setActiveStatus.js +0 -92
  260. package/src/setMessageReaction.js +0 -150
  261. package/src/setMessageReactionMqtt.js +0 -104
  262. package/src/setMessageReactionSafe.js +0 -44
  263. package/src/setProfileGuard.js +0 -44
  264. package/src/setProfileLock.js +0 -97
  265. package/src/setStoryReaction.js +0 -133
  266. package/src/setStorySeen.js +0 -108
  267. package/src/setThreadTheme.js +0 -102
  268. package/src/setTitle.js +0 -92
  269. package/src/shareContact.js +0 -110
  270. package/src/shareLink.js +0 -59
  271. package/src/stopListenMqtt.js +0 -60
  272. package/src/storyManager.js +0 -357
  273. package/src/suggestFriend.js +0 -132
  274. package/src/unsendMessage.js +0 -68
  275. package/src/uploadAttachment.js +0 -115
  276. package/test/fixes-1_2_4.test.js +0 -118
  277. package/test/mqtt-stop.test.js +0 -32
  278. package/test/reaction-mqtt.test.js +0 -57
  279. package/utils.js +0 -3056
  280. /package/{build → src/api/socket/e2ee/native/build}/messagix.dll +0 -0
  281. /package/{build → src/api/socket/e2ee/native/build}/messagix.so +0 -0
@@ -0,0 +1,1413 @@
1
+ "use strict";
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const EventEmitter = require("events");
5
+ const models = require("../src/database/models");
6
+ const logger = require("../func/logger");
7
+ const { get, post, jar, makeDefaults } = require("../src/utils/request");
8
+ const { saveCookies, getAppState } = require("../src/utils/client");
9
+ const { getFrom } = require("../src/utils/constants");
10
+ const { loadConfig } = require("./config");
11
+ const { createRemoteClient } = require("../src/remote/remoteClient");
12
+ const { getDefaultUserAgent } = require("../src/utils/fingerprint");
13
+ const { config } = loadConfig();
14
+ const axiosBase = require("axios");
15
+ const regions = [
16
+ { code: "PRN", name: "Pacific Northwest Region", location: "Khu vực Tây Bắc Thái Bình Dương" },
17
+ { code: "VLL", name: "Valley Region", location: "Valley" },
18
+ { code: "ASH", name: "Ashburn Region", location: "Ashburn" },
19
+ { code: "DFW", name: "Dallas/Fort Worth Region", location: "Dallas/Fort Worth" },
20
+ { code: "LLA", name: "Los Angeles Region", location: "Los Angeles" },
21
+ { code: "FRA", name: "Frankfurt", location: "Frankfurt" },
22
+ { code: "SIN", name: "Singapore", location: "Singapore" },
23
+ { code: "NRT", name: "Tokyo", location: "Japan" },
24
+ { code: "HKG", name: "Hong Kong", location: "Hong Kong" },
25
+ { code: "SYD", name: "Sydney", location: "Sydney" },
26
+ { code: "PNB", name: "Pacific Northwest - Beta", location: "Pacific Northwest " }
27
+ ];
28
+
29
+ const REGION_MAP = new Map(regions.map(r => [r.code, r]));
30
+
31
+ function parseRegion(html) {
32
+ try {
33
+ const m1 = html.match(/"endpoint":"([^"]+)"/);
34
+ const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
35
+ const raw = (m1 && m1[1]) || (m2 && m2[1]);
36
+ if (!raw) return "PRN";
37
+ const endpoint = raw.replace(/\\\//g, "/");
38
+ const url = new URL(endpoint);
39
+ const rp = url.searchParams ? url.searchParams.get("region") : null;
40
+ return rp ? rp.toUpperCase() : "PRN";
41
+ } catch {
42
+ return "PRN";
43
+ }
44
+ }
45
+
46
+ function mask(s, keep = 3) {
47
+ if (!s) return "";
48
+ const n = s.length;
49
+ return n <= keep ? "*".repeat(n) : s.slice(0, keep) + "*".repeat(Math.max(0, n - keep));
50
+ }
51
+
52
+ /**
53
+ * Login via external API endpoint (iOS method)
54
+ * @param {string} email - Email hoặc số điện thoại
55
+ * @param {string} password - Mật khẩu
56
+ * @param {string|null} twoFactor - Secret Base32 cho 2FA (không phải mã 6 số)
57
+ * @param {string|null} apiBaseUrl - Base URL của API server (required; no default - credentials are sent to this host)
58
+ * @param {string|null} apiKey - API key để xác thực (x-api-key header)
59
+ * @returns {Promise<{ok: boolean, uid?: string, access_token?: string, cookies?: Array, cookie?: string, message?: string}>}
60
+ */
61
+ async function loginViaAPI(email, password, twoFactor = null, apiBaseUrl = null, apiKey = null) {
62
+ try {
63
+ const baseUrl = apiBaseUrl || config.apiServer;
64
+ if (!baseUrl) {
65
+ const msg = "Credential login needs an explicit apiServer (fca-config.json or argument); none is configured. Use appState/cookies instead.";
66
+ logger(`API-LOGIN: ${msg}`, "error");
67
+ return { ok: false, message: msg };
68
+ }
69
+ const endpoint = `${baseUrl}/api/v1/facebook/login_ios`;
70
+ const xApiKey = apiKey || config.apiKey || null;
71
+
72
+ // Build request body
73
+ const body = {
74
+ email,
75
+ password
76
+ };
77
+
78
+ // Only include twoFactor if provided (must be Base32 secret, not 6-digit code)
79
+ if (twoFactor && typeof twoFactor === "string" && twoFactor.trim()) {
80
+ // Clean up the secret - remove spaces and convert to uppercase
81
+ body.twoFactor = twoFactor.replace(/\s+/g, "").toUpperCase();
82
+ }
83
+
84
+ // Build headers
85
+ const headers = {
86
+ "Content-Type": "application/json",
87
+ "Accept": "application/json"
88
+ };
89
+
90
+ // Add x-api-key header if provided
91
+ if (xApiKey) {
92
+ headers["x-api-key"] = xApiKey;
93
+ }
94
+
95
+ logger(`API-LOGIN: Attempting login for ${mask(email, 2)} via iOS API`, "info");
96
+
97
+ const response = await axiosBase({
98
+ method: "POST",
99
+ url: endpoint,
100
+ headers,
101
+ data: body,
102
+ timeout: 60000,
103
+ validateStatus: () => true
104
+ });
105
+ if (response.status === 200 && response.data) {
106
+ const data = response.data;
107
+
108
+ // Check if login was successful
109
+ if (data.error) {
110
+ logger(`API-LOGIN: Login failed - ${data.error}`, "error");
111
+ return { ok: false, message: data.error };
112
+ }
113
+
114
+ // Extract response data
115
+ const uid = data.uid || data.user_id || data.userId || null;
116
+ const accessToken = data.access_token || data.accessToken || null;
117
+ const cookie = data.cookie || data.cookies || null;
118
+
119
+ if (!uid && !accessToken && !cookie) {
120
+ logger("API-LOGIN: Response missing required fields (uid, access_token, cookie)", "warn");
121
+ return { ok: false, message: "Invalid response from API" };
122
+ }
123
+
124
+ logger(`API-LOGIN: Login successful for UID: ${uid || "unknown"}`, "info");
125
+
126
+ // Parse cookies if provided as string
127
+ let cookies = [];
128
+ if (typeof cookie === "string") {
129
+ // Parse cookie string format: "key1=value1; key2=value2"
130
+ const pairs = cookie.split(";").map(p => p.trim()).filter(Boolean);
131
+ for (const pair of pairs) {
132
+ const eq = pair.indexOf("=");
133
+ if (eq > 0) {
134
+ const key = pair.slice(0, eq).trim();
135
+ const value = pair.slice(eq + 1).trim();
136
+ cookies.push({
137
+ key,
138
+ value,
139
+ domain: ".facebook.com",
140
+ path: "/"
141
+ });
142
+ }
143
+ }
144
+ } else if (Array.isArray(cookie)) {
145
+ // Already in array format
146
+ cookies = cookie.map(c => ({
147
+ key: c.key || c.name,
148
+ value: c.value,
149
+ domain: c.domain || ".facebook.com",
150
+ path: c.path || "/"
151
+ }));
152
+ }
153
+
154
+ return {
155
+ ok: true,
156
+ uid,
157
+ access_token: accessToken,
158
+ cookies,
159
+ cookie: typeof cookie === "string" ? cookie : null
160
+ };
161
+ }
162
+
163
+ // Handle error responses
164
+ const errorMsg = response.data && response.data.error
165
+ ? response.data.error
166
+ : response.data && response.data.message
167
+ ? response.data.message
168
+ : `HTTP ${response.status}`;
169
+
170
+ logger(`API-LOGIN: Login failed - ${errorMsg}`, "error");
171
+ return { ok: false, message: errorMsg };
172
+
173
+ } catch (error) {
174
+ const errMsg = error && error.message ? error.message : String(error);
175
+ logger(`API-LOGIN: Request failed - ${errMsg}`, "error");
176
+ return { ok: false, message: errMsg };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * High-level login function that uses the API endpoint
182
+ * @param {string} email - Email hoặc số điện thoại
183
+ * @param {string} password - Mật khẩu
184
+ * @param {string|null} twoFactor - Secret Base32 cho 2FA (không phải mã 6 số)
185
+ * @param {string|null} apiBaseUrl - Base URL của API server
186
+ * @returns {Promise<{status: boolean, cookies?: Array, uid?: string, access_token?: string, message?: string}>}
187
+ */
188
+ async function tokensViaAPI(email, password, twoFactor = null, apiBaseUrl = null) {
189
+ const t0 = process.hrtime.bigint();
190
+
191
+ if (!email || !password) {
192
+ return { status: false, message: "Please provide email and password" };
193
+ }
194
+
195
+ logger(`API-LOGIN: Initialize login ${mask(email, 2)}`, "info");
196
+
197
+ const res = await loginViaAPI(email, password, twoFactor, apiBaseUrl);
198
+
199
+ if (res && res.ok) {
200
+ logger(`API-LOGIN: Login success - UID: ${res.uid}`, "info");
201
+ const t1 = Number(process.hrtime.bigint() - t0) / 1e6;
202
+ logger(`Done API login ${Math.round(t1)}ms`, "info");
203
+
204
+ return {
205
+ status: true,
206
+ cookies: res.cookies,
207
+ uid: res.uid,
208
+ access_token: res.access_token,
209
+ cookie: res.cookie
210
+ };
211
+ }
212
+
213
+ return {
214
+ status: false,
215
+ message: res && res.message ? res.message : "Login failed"
216
+ };
217
+ }
218
+
219
+ function normalizeCookieHeaderString(s) {
220
+ let str = String(s || "").trim();
221
+ if (!str) return [];
222
+ if (/^cookie\s*:/i.test(str)) str = str.replace(/^cookie\s*:/i, "").trim();
223
+ str = str.replace(/\r?\n/g, " ").replace(/\s*;\s*/g, ";");
224
+ const parts = str.split(";").map(v => v.trim()).filter(Boolean);
225
+ const out = [];
226
+ for (const p of parts) {
227
+ const eq = p.indexOf("=");
228
+ if (eq <= 0) continue;
229
+ const k = p.slice(0, eq).trim();
230
+ const v = p.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
231
+ if (!k) continue;
232
+ out.push(`${k}=${v}`);
233
+ }
234
+ return out;
235
+ }
236
+
237
+ function setJarFromPairs(j, pairs, domain) {
238
+ const expires = new Date(Date.now() + 31536e6).toUTCString();
239
+ // URLs to set cookies for - include both desktop and mobile versions
240
+ const urls = [
241
+ "https://www.facebook.com",
242
+ "https://facebook.com",
243
+ "https://m.facebook.com",
244
+ "http://www.facebook.com",
245
+ "http://facebook.com",
246
+ "http://m.facebook.com"
247
+ ];
248
+
249
+ for (const kv of pairs) {
250
+ const cookieStr = `${kv}; expires=${expires}; domain=${domain}; path=/;`;
251
+ // Set cookie for all URLs to ensure it works on both desktop and mobile
252
+ for (const url of urls) {
253
+ try {
254
+ if (typeof j.setCookieSync === "function") {
255
+ j.setCookieSync(cookieStr, url);
256
+ } else if (typeof j.setCookie === "function") {
257
+ j.setCookie(cookieStr, url);
258
+ }
259
+ } catch (err) {
260
+ // Silently ignore domain mismatch errors
261
+ // These can happen when setting cookies across different subdomains
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ function cookieHeaderFromJar(j) {
268
+ const urls = ["https://www.facebook.com"];
269
+ const seen = new Set();
270
+ const parts = [];
271
+ for (const u of urls) {
272
+ let s = "";
273
+ try {
274
+ s = typeof j.getCookieStringSync === "function" ? j.getCookieStringSync(u) : "";
275
+ } catch { }
276
+ if (!s) continue;
277
+ for (const kv of s.split(";")) {
278
+ const t = kv.trim();
279
+ const name = t.split("=")[0];
280
+ if (!name || seen.has(name)) continue;
281
+ seen.add(name);
282
+ parts.push(t);
283
+ }
284
+ }
285
+ return parts.join("; ");
286
+ }
287
+
288
+ let uniqueIndexEnsured = false;
289
+
290
+ function getBackupModel() {
291
+ try {
292
+ if (!models || !models.sequelize || !models.Sequelize) return null;
293
+ const sequelize = models.sequelize;
294
+
295
+ // Validate that sequelize is a proper Sequelize instance
296
+ if (!sequelize || typeof sequelize.define !== "function") return null;
297
+
298
+ const { DataTypes } = models.Sequelize;
299
+ if (sequelize.models && sequelize.models.AppStateBackup) return sequelize.models.AppStateBackup;
300
+ const dialect = typeof sequelize.getDialect === "function" ? sequelize.getDialect() : "sqlite";
301
+ const LongText = (dialect === "mysql" || dialect === "mariadb") ? DataTypes.TEXT("long") : DataTypes.TEXT;
302
+
303
+ try {
304
+ const AppStateBackup = sequelize.define(
305
+ "AppStateBackup",
306
+ {
307
+ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
308
+ userID: { type: DataTypes.STRING, allowNull: false },
309
+ type: { type: DataTypes.STRING, allowNull: false },
310
+ data: { type: LongText }
311
+ },
312
+ { tableName: "app_state_backups", timestamps: true, indexes: [{ unique: true, fields: ["userID", "type"] }] }
313
+ );
314
+ return AppStateBackup;
315
+ } catch (defineError) {
316
+ // If define fails, log and return null
317
+ logger(`Failed to define AppStateBackup model: ${defineError && defineError.message ? defineError.message : String(defineError)}`, "warn");
318
+ return null;
319
+ }
320
+ } catch (e) {
321
+ // Silently handle any errors in getBackupModel
322
+ return null;
323
+ }
324
+ }
325
+
326
+ async function ensureUniqueIndex(sequelize) {
327
+ if (uniqueIndexEnsured || !sequelize) return;
328
+ try {
329
+ if (typeof sequelize.getQueryInterface !== "function") return;
330
+ await sequelize.getQueryInterface().addIndex("app_state_backups", ["userID", "type"], { unique: true, name: "app_state_user_type_unique" });
331
+ } catch { }
332
+ uniqueIndexEnsured = true;
333
+ }
334
+
335
+ async function upsertBackup(Model, userID, type, data) {
336
+ const where = { userID: String(userID || ""), type };
337
+ const row = await Model.findOne({ where });
338
+ if (row) {
339
+ await row.update({ data });
340
+ logger(`Overwrote existing ${type} backup for user ${where.userID}`, "info");
341
+ return;
342
+ }
343
+ await Model.create({ ...where, data });
344
+ logger(`Created new ${type} backup for user ${where.userID}`, "info");
345
+ }
346
+
347
+ async function backupAppStateSQL(j, userID) {
348
+ try {
349
+ const Model = getBackupModel();
350
+ if (!Model) return;
351
+ if (!models || !models.sequelize) return;
352
+ await Model.sync();
353
+ await ensureUniqueIndex(models.sequelize);
354
+ const appJson = getAppState(j);
355
+ const ck = cookieHeaderFromJar(j);
356
+ await upsertBackup(Model, userID, "appstate", JSON.stringify(appJson));
357
+ await upsertBackup(Model, userID, "cookie", ck);
358
+ logger("Backup stored (overwrite mode)", "info");
359
+ } catch (e) {
360
+ logger(`Failed to save appstate backup ${e && e.message ? e.message : String(e)}`, "warn");
361
+ }
362
+ }
363
+
364
+ async function getLatestBackup(userID, type) {
365
+ try {
366
+ const Model = getBackupModel();
367
+ if (!Model) return null;
368
+ const row = await Model.findOne({ where: { userID: String(userID || ""), type } });
369
+ return row ? row.data : null;
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+
375
+ async function getLatestBackupAny(type) {
376
+ try {
377
+ const Model = getBackupModel();
378
+ if (!Model) return null;
379
+ const row = await Model.findOne({ where: { type }, order: [["updatedAt", "DESC"]] });
380
+ return row ? row.data : null;
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+
386
+
387
+
388
+ async function setJarCookies(j, appstate) {
389
+ const tasks = [];
390
+ for (const c of appstate) {
391
+ const cookieName = c.name || c.key;
392
+ const cookieValue = c.value;
393
+ if (!cookieName || cookieValue === undefined) continue;
394
+
395
+ const cookieDomain = c.domain || ".facebook.com";
396
+ const cookiePath = c.path || "/";
397
+ const dom = cookieDomain.replace(/^\./, "");
398
+
399
+ // Handle expirationDate (can be in seconds or milliseconds)
400
+ let expiresStr = "";
401
+ if (c.expirationDate !== undefined) {
402
+ let expiresDate;
403
+ if (typeof c.expirationDate === "number") {
404
+ // If expirationDate is less than a year from now in seconds, treat as seconds
405
+ // Otherwise treat as milliseconds
406
+ const now = Date.now();
407
+ const oneYearInMs = 365 * 24 * 60 * 60 * 1000;
408
+ if (c.expirationDate < (now + oneYearInMs) / 1000) {
409
+ expiresDate = new Date(c.expirationDate * 1000);
410
+ } else {
411
+ expiresDate = new Date(c.expirationDate);
412
+ }
413
+ } else {
414
+ expiresDate = new Date(c.expirationDate);
415
+ }
416
+ expiresStr = `; expires=${expiresDate.toUTCString()}`;
417
+ } else if (c.expires) {
418
+ const expiresDate = typeof c.expires === "number" ? new Date(c.expires) : new Date(c.expires);
419
+ expiresStr = `; expires=${expiresDate.toUTCString()}`;
420
+ }
421
+
422
+ // Helper function to build cookie string
423
+ const buildCookieString = (domainOverride = null) => {
424
+ const domain = domainOverride || cookieDomain;
425
+ let cookieParts = [`${cookieName}=${cookieValue}${expiresStr}`];
426
+ cookieParts.push(`Domain=${domain}`);
427
+ cookieParts.push(`Path=${cookiePath}`);
428
+
429
+ // Add Secure flag if secure is true
430
+ if (c.secure === true) {
431
+ cookieParts.push("Secure");
432
+ }
433
+
434
+ // Add HttpOnly flag if httpOnly is true
435
+ if (c.httpOnly === true) {
436
+ cookieParts.push("HttpOnly");
437
+ }
438
+
439
+ // Add SameSite attribute if provided
440
+ if (c.sameSite) {
441
+ const sameSiteValue = String(c.sameSite).toLowerCase();
442
+ if (["strict", "lax", "none"].includes(sameSiteValue)) {
443
+ cookieParts.push(`SameSite=${sameSiteValue.charAt(0).toUpperCase() + sameSiteValue.slice(1)}`);
444
+ }
445
+ }
446
+
447
+ return cookieParts.join("; ");
448
+ };
449
+ const cookieConfigs = [];
450
+ if (cookieDomain === ".facebook.com" || cookieDomain === "facebook.com") {
451
+ cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
452
+ cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
453
+ cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
454
+ cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
455
+ } else {
456
+ cookieConfigs.push({ url: `http://${dom}${cookiePath}`, cookieStr: buildCookieString() });
457
+ cookieConfigs.push({ url: `https://${dom}${cookiePath}`, cookieStr: buildCookieString() });
458
+ cookieConfigs.push({ url: `http://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
459
+ cookieConfigs.push({ url: `https://www.${dom}${cookiePath}`, cookieStr: buildCookieString() });
460
+ }
461
+
462
+ for (const config of cookieConfigs) {
463
+ tasks.push(j.setCookie(config.cookieStr, config.url).catch((err) => {
464
+ if (err && err.message && err.message.includes("Cookie not in this host's domain")) {
465
+ return;
466
+ }
467
+ return;
468
+ }));
469
+ }
470
+ }
471
+ await Promise.all(tasks);
472
+ }
473
+
474
+ // tokens function - alias to tokensViaAPI for backward compatibility
475
+ async function tokens(username, password, twofactor = null) {
476
+ return tokensViaAPI(username, password, twofactor);
477
+ }
478
+
479
+ async function hydrateJarFromDB(userID) {
480
+ try {
481
+ let ck = null;
482
+ let app = null;
483
+ if (userID) {
484
+ ck = await getLatestBackup(userID, "cookie");
485
+ app = await getLatestBackup(userID, "appstate");
486
+ } else {
487
+ ck = await getLatestBackupAny("cookie");
488
+ app = await getLatestBackupAny("appstate");
489
+ }
490
+ if (ck) {
491
+ const pairs = normalizeCookieHeaderString(ck);
492
+ if (pairs.length) {
493
+ setJarFromPairs(jar, pairs, ".facebook.com");
494
+ return true;
495
+ }
496
+ }
497
+ if (app) {
498
+ let parsed = null;
499
+ try {
500
+ parsed = JSON.parse(app);
501
+ } catch { }
502
+ if (Array.isArray(parsed)) {
503
+ const pairs = parsed.map(c => [c.name || c.key, c.value].join("="));
504
+ setJarFromPairs(jar, pairs, ".facebook.com");
505
+ return true;
506
+ }
507
+ }
508
+ return false;
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+
514
+ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef, hadAppStateInput = false) {
515
+ // Helper to validate UID - must be a non-zero positive number string
516
+ const isValidUID = uid => uid && uid !== "0" && /^\d+$/.test(uid) && parseInt(uid, 10) > 0;
517
+
518
+ const getUID = cs =>
519
+ cs.find(c => c.key === "i_user")?.value ||
520
+ cs.find(c => c.key === "c_user")?.value ||
521
+ cs.find(c => c.name === "i_user")?.value ||
522
+ cs.find(c => c.name === "c_user")?.value;
523
+ const htmlUID = body => {
524
+ const s = typeof body === "string" ? body : String(body ?? "");
525
+ return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
526
+ };
527
+
528
+ let userID = getUID(currentCookies);
529
+ // Also try to extract userID from HTML if cookie userID is invalid
530
+ if (!isValidUID(userID)) {
531
+ userID = htmlUID(currentHtml);
532
+ }
533
+ // If we have a valid userID, return success
534
+ if (isValidUID(userID)) {
535
+ return { html: currentHtml, cookies: currentCookies, userID };
536
+ }
537
+
538
+ // No valid userID found - need to try auto-login
539
+ logger("tryAutoLoginIfNeeded: No valid userID found, attempting recovery...", "warn");
540
+
541
+ // If appState/Cookie was provided and is not checkpointed, try refresh
542
+ if (hadAppStateInput) {
543
+ const isCheckpoint = currentHtml.includes("/checkpoint/block/?next");
544
+ if (!isCheckpoint) {
545
+ try {
546
+ const refreshedCookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
547
+ userID = getUID(refreshedCookies);
548
+ if (isValidUID(userID)) {
549
+ return { html: currentHtml, cookies: refreshedCookies, userID };
550
+ }
551
+ } catch { }
552
+ }
553
+ }
554
+
555
+ // Try to hydrate from DB backup
556
+ const hydrated = await hydrateJarFromDB(null);
557
+ if (hydrated) {
558
+ logger("tryAutoLoginIfNeeded: Trying backup from DB...", "info");
559
+ try {
560
+ const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
561
+ const resB = (await ctxRef.bypassAutomation(initial, jar)) || initial;
562
+ const htmlB = resB && resB.data ? resB.data : "";
563
+ if (!htmlB.includes("/checkpoint/block/?next")) {
564
+ const htmlUserID = htmlUID(htmlB);
565
+ if (isValidUID(htmlUserID)) {
566
+ const cookiesB = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
567
+ logger(`tryAutoLoginIfNeeded: DB backup session valid, USER_ID=${htmlUserID}`, "info");
568
+ return { html: htmlB, cookies: cookiesB, userID: htmlUserID };
569
+ } else {
570
+ logger(`tryAutoLoginIfNeeded: DB backup session dead (HTML USER_ID=${htmlUserID || "empty"}), will try API login...`, "warn");
571
+ }
572
+ }
573
+ } catch (dbErr) {
574
+ logger(`tryAutoLoginIfNeeded: DB backup failed - ${dbErr && dbErr.message ? dbErr.message : String(dbErr)}`, "warn");
575
+ }
576
+ }
577
+
578
+ if (config.autoLogin !== true && config.autoLogin !== "true") {
579
+ throw new Error("AppState expired — provide a fresh appstate.json (auto-login via external API is disabled by default)");
580
+ }
581
+
582
+ // Try API login
583
+ const u = config.credentials?.email || config.email;
584
+ const p = config.credentials?.password || config.password;
585
+ const tf = config.credentials?.twofactor || config.twofactor || null;
586
+
587
+ if (!u || !p) {
588
+ logger("tryAutoLoginIfNeeded: No credentials configured for auto-login!", "error");
589
+ throw new Error("Missing credentials for auto-login (email/password not configured in fca-config.json)");
590
+ }
591
+
592
+ logger(`tryAutoLoginIfNeeded: Attempting API login for ${u.slice(0, 3)}***...`, "info");
593
+
594
+ const r = await tokens(u, p, tf);
595
+ if (!r || !r.status) {
596
+ throw new Error(r && r.message ? r.message : "API Login failed");
597
+ }
598
+
599
+ logger(`tryAutoLoginIfNeeded: API login successful! UID: ${r.uid}`, "info");
600
+
601
+ // Handle cookies - can be array, cookie string header, or both
602
+ let cookiePairs = [];
603
+
604
+ // If cookies is a string (cookie header format), parse it
605
+ if (typeof r.cookies === "string") {
606
+ cookiePairs = normalizeCookieHeaderString(r.cookies);
607
+ }
608
+ // If cookies is an array, convert to pairs
609
+ else if (Array.isArray(r.cookies)) {
610
+ cookiePairs = r.cookies.map(c => {
611
+ if (typeof c === "string") {
612
+ // Already in "key=value" format
613
+ return c;
614
+ } else if (c && typeof c === "object") {
615
+ // Object format {key, value} or {name, value}
616
+ return `${c.key || c.name}=${c.value}`;
617
+ }
618
+ return null;
619
+ }).filter(Boolean);
620
+ }
621
+
622
+ // Also check for cookie field (alternative field name)
623
+ if (cookiePairs.length === 0 && r.cookie) {
624
+ if (typeof r.cookie === "string") {
625
+ cookiePairs = normalizeCookieHeaderString(r.cookie);
626
+ } else if (Array.isArray(r.cookie)) {
627
+ cookiePairs = r.cookie.map(c => {
628
+ if (typeof c === "string") return c;
629
+ if (c && typeof c === "object") return `${c.key || c.name}=${c.value}`;
630
+ return null;
631
+ }).filter(Boolean);
632
+ }
633
+ }
634
+
635
+ if (cookiePairs.length === 0) {
636
+ logger("tryAutoLoginIfNeeded: No cookies found in API response", "warn");
637
+ throw new Error("API login returned no cookies");
638
+ } else {
639
+ logger(`tryAutoLoginIfNeeded: Parsed ${cookiePairs.length} cookies from API response`, "info");
640
+ setJarFromPairs(jar, cookiePairs, ".facebook.com");
641
+ }
642
+
643
+ // Wait a bit for cookies to be set
644
+ await new Promise(resolve => setTimeout(resolve, 500));
645
+
646
+ // Refresh Facebook page with new cookies - try multiple times if needed
647
+ // Try both www.facebook.com and m.facebook.com to ensure session is established
648
+ let html2 = "";
649
+ let res2 = null;
650
+ let retryCount = 0;
651
+ const maxRetries = 3;
652
+ const urlsToTry = ["https://m.facebook.com/", "https://www.facebook.com/"];
653
+
654
+ while (retryCount < maxRetries) {
655
+ try {
656
+ // Try m.facebook.com first (mobile version often works better for API login)
657
+ const urlToUse = retryCount === 0 ? urlsToTry[0] : urlsToTry[retryCount % urlsToTry.length];
658
+ logger(`tryAutoLoginIfNeeded: Refreshing ${urlToUse} (attempt ${retryCount + 1}/${maxRetries})...`, "info");
659
+
660
+ const initial2 = await get(urlToUse, jar, null, globalOptions).then(saveCookies(jar));
661
+ res2 = (await ctxRef.bypassAutomation(initial2, jar)) || initial2;
662
+ html2 = res2 && res2.data ? res2.data : "";
663
+
664
+ if (html2.includes("/checkpoint/block/?next")) {
665
+ throw new Error("Checkpoint after API login");
666
+ }
667
+
668
+ // Check if HTML contains valid USER_ID
669
+ const htmlUserID = htmlUID(html2);
670
+ if (isValidUID(htmlUserID)) {
671
+ logger(`tryAutoLoginIfNeeded: Found valid USER_ID in HTML from ${urlToUse}: ${htmlUserID}`, "info");
672
+ break;
673
+ }
674
+
675
+ // If no valid USER_ID found, wait and retry with different URL
676
+ if (retryCount < maxRetries - 1) {
677
+ logger(`tryAutoLoginIfNeeded: No valid USER_ID in HTML from ${urlToUse} (attempt ${retryCount + 1}/${maxRetries}), retrying...`, "warn");
678
+ await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
679
+ retryCount++;
680
+ } else {
681
+ logger("tryAutoLoginIfNeeded: No valid USER_ID found in HTML after retries", "warn");
682
+ break;
683
+ }
684
+ } catch (err) {
685
+ if (err.message && err.message.includes("Checkpoint")) {
686
+ throw err;
687
+ }
688
+ if (retryCount < maxRetries - 1) {
689
+ logger(`tryAutoLoginIfNeeded: Error refreshing page (attempt ${retryCount + 1}/${maxRetries}): ${err && err.message ? err.message : String(err)}`, "warn");
690
+ await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
691
+ retryCount++;
692
+ } else {
693
+ throw err;
694
+ }
695
+ }
696
+ }
697
+
698
+ const cookies2 = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
699
+ const uid2 = getUID(cookies2);
700
+ const htmlUserID2 = htmlUID(html2);
701
+
702
+ // Prioritize USER_ID from HTML over cookies (more reliable)
703
+ let finalUID = null;
704
+ if (isValidUID(htmlUserID2)) {
705
+ finalUID = htmlUserID2;
706
+ logger(`tryAutoLoginIfNeeded: Using USER_ID from HTML: ${finalUID}`, "info");
707
+ } else if (isValidUID(uid2)) {
708
+ finalUID = uid2;
709
+ logger(`tryAutoLoginIfNeeded: Using USER_ID from cookies: ${finalUID}`, "info");
710
+ } else if (isValidUID(r.uid)) {
711
+ finalUID = r.uid;
712
+ logger(`tryAutoLoginIfNeeded: Using USER_ID from API response: ${finalUID}`, "info");
713
+ }
714
+
715
+ if (!isValidUID(finalUID)) {
716
+ logger(`tryAutoLoginIfNeeded: HTML check - USER_ID from HTML: ${htmlUserID2 || "none"}, from cookies: ${uid2 || "none"}, from API: ${r.uid || "none"}`, "error");
717
+ throw new Error("Login failed - could not get valid userID after API login. HTML may indicate session is not established.");
718
+ }
719
+
720
+ // Final validation: ensure HTML shows we're logged in
721
+ if (!isValidUID(htmlUserID2)) {
722
+ logger("tryAutoLoginIfNeeded: WARNING - HTML does not show valid USER_ID, but proceeding with cookie-based UID", "warn");
723
+ }
724
+
725
+ return { html: html2, cookies: cookies2, userID: finalUID };
726
+ }
727
+
728
+ function makeLogin(j, email, password, globalOptions) {
729
+ return async function () {
730
+ const u = email || config.credentials?.email;
731
+ const p = password || config.credentials?.password;
732
+ const tf = config.credentials?.twofactor || null;
733
+ if (!u || !p) return;
734
+ const r = await tokens(u, p, tf);
735
+ if (r && r.status && Array.isArray(r.cookies)) {
736
+ const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
737
+ setJarFromPairs(j, pairs, ".facebook.com");
738
+ await get("https://www.facebook.com/", j, null, globalOptions).then(saveCookies(j));
739
+ } else {
740
+ throw new Error(r && r.message ? r.message : "Login failed");
741
+ }
742
+ };
743
+ }
744
+
745
+ function loginHelper(appState, Cookie, email, password, globalOptions, callback) {
746
+ try {
747
+ const domain = ".facebook.com";
748
+ // Helper to extract userID from appState input
749
+ const extractUIDFromAppState = (appStateInput) => {
750
+ if (!appStateInput) return null;
751
+ let parsed = appStateInput;
752
+ if (typeof appStateInput === "string") {
753
+ try {
754
+ parsed = JSON.parse(appStateInput);
755
+ } catch {
756
+ return null;
757
+ }
758
+ }
759
+ if (Array.isArray(parsed)) {
760
+ const cUser = parsed.find(c => (c.key === "c_user" || c.name === "c_user"));
761
+ if (cUser) return cUser.value;
762
+ const iUser = parsed.find(c => (c.key === "i_user" || c.name === "i_user"));
763
+ if (iUser) return iUser.value;
764
+ }
765
+ return null;
766
+ };
767
+ let userIDFromAppState = extractUIDFromAppState(appState);
768
+ (async () => {
769
+ try {
770
+ if (appState) {
771
+ // Check and convert cookie to appState format
772
+ if (Array.isArray(appState) && appState.some(c => c.name)) {
773
+ // Convert name to key if needed
774
+ appState = appState.map(c => {
775
+ if (c.name && !c.key) {
776
+ c.key = c.name;
777
+ delete c.name;
778
+ }
779
+ return c;
780
+ });
781
+ } else if (typeof appState === "string") {
782
+ // Try to parse as JSON first
783
+ let parsed = appState;
784
+ try {
785
+ parsed = JSON.parse(appState);
786
+ } catch { }
787
+
788
+ if (Array.isArray(parsed)) {
789
+ // Already parsed as array, use it
790
+ appState = parsed;
791
+ } else {
792
+ // Parse string cookie format (key=value; key2=value2)
793
+ const arrayAppState = [];
794
+ appState.split(';').forEach(c => {
795
+ const [key, value] = c.split('=');
796
+ if (key && value) {
797
+ arrayAppState.push({
798
+ key: key.trim(),
799
+ value: value.trim(),
800
+ domain: ".facebook.com",
801
+ path: "/",
802
+ expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 365
803
+ });
804
+ }
805
+ });
806
+ appState = arrayAppState;
807
+ }
808
+ }
809
+
810
+ // Set cookies into jar with individual domain/path
811
+ if (Array.isArray(appState)) {
812
+ await setJarCookies(jar, appState);
813
+ } else {
814
+ throw new Error("Invalid appState format");
815
+ }
816
+ }
817
+ if (Cookie) {
818
+ let cookiePairs = [];
819
+ if (typeof Cookie === "string") cookiePairs = normalizeCookieHeaderString(Cookie);
820
+ else if (Array.isArray(Cookie)) cookiePairs = Cookie.map(String).filter(Boolean);
821
+ else if (Cookie && typeof Cookie === "object") cookiePairs = Object.entries(Cookie).map(([k, v]) => `${k}=${v}`);
822
+ if (cookiePairs.length) setJarFromPairs(jar, cookiePairs, domain);
823
+ }
824
+ } catch (e) {
825
+ return callback(e);
826
+ }
827
+ const ctx = { globalOptions, options: globalOptions, reconnectAttempts: 0 };
828
+ ctx.bypassAutomation = async function (resp, j) {
829
+ global.fca = global.fca || {};
830
+ global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
831
+ const s = x => (typeof x === "string" ? x : String(x ?? ""));
832
+ const u = r => r?.request?.res?.responseUrl || (r?.config?.baseURL ? new URL(r.config.url || "/", r.config.baseURL).toString() : r?.config?.url || "");
833
+ const isCp = r => typeof u(r) === "string" && u(r).includes("checkpoint/601051028565049");
834
+ const cookieUID = async () => {
835
+ try {
836
+ const cookies = typeof j?.getCookies === "function" ? await j.getCookies("https://www.facebook.com") : [];
837
+ return cookies.find(c => c.key === "i_user")?.value || cookies.find(c => c.key === "c_user")?.value;
838
+ } catch { return undefined; }
839
+ };
840
+ const htmlUID = body => s(body).match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s(body).match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
841
+ const getUID = async body => (await cookieUID()) || htmlUID(body);
842
+ const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
843
+ const bypass = async body => {
844
+ const b = s(body);
845
+ const UID = await getUID(b);
846
+ const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
847
+ const jazoest = getFrom(b, 'name="jazoest" value="', '"') || getFrom(b, "jazoest=", '",') || b.match(/name="jazoest"\s+value="([^"]+)"/)?.[1];
848
+ const lsd = getFrom(b, '["LSD",[],{"token":"', '"}') || b.match(/name="lsd"\s+value="([^"]+)"/)?.[1];
849
+ const form = { av: UID, fb_dtsg, jazoest, lsd, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: "{}", server_timestamps: true, doc_id: 6339492849481770 };
850
+ await post("https://www.facebook.com/api/graphql/", j, form, null, this.options).then(saveCookies(j));
851
+ logger("⚠️ Facebook automation detected — bypassing...", "warn");
852
+ this.reconnectAttempts = 0;
853
+ };
854
+ try {
855
+ if (resp) {
856
+ if (isCp(resp)) {
857
+ await bypass(s(resp.data));
858
+ const refreshed = await refreshJar();
859
+ if (isCp(refreshed)) logger("Checkpoint still present after refresh", "warn");
860
+ else logger("✅ Bypass complete — session restored.", "info");
861
+ return refreshed;
862
+ }
863
+ return resp;
864
+ }
865
+ const first = await get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
866
+ if (isCp(first)) {
867
+ await bypass(s(first.data));
868
+ const refreshed = await refreshJar();
869
+ if (!isCp(refreshed)) logger("✅ Bypass complete — session restored.", "info");
870
+ else logger("Checkpoint still present after refresh", "warn");
871
+ return refreshed;
872
+ }
873
+ return first;
874
+ } catch (e) {
875
+ logger(`Bypass automation error: ${e && e.message ? e.message : String(e)}`, "error");
876
+ return resp;
877
+ }
878
+ };
879
+ if (appState || Cookie) {
880
+ const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
881
+ // Sanity-check the response actually looks like a real FB page load
882
+ // for the current UA (checked via a marker only present in that
883
+ // rendering). If not, the appstate/UA combo may be mismatched from
884
+ // how it was originally captured — retry once with a known-good UA
885
+ // rather than pushing forward with a session that already looks
886
+ // inconsistent to Facebook.
887
+ const bodyStr = typeof initial?.data === "string" ? initial.data : String(initial?.data ?? "");
888
+ if (!/MPageLoadClientMetrics/.test(bodyStr)) {
889
+ logger("Initial page load doesn't match expected UA rendering — retrying with fallback UA", "warn");
890
+ globalOptions.userAgent = getDefaultUserAgent();
891
+ const retryInitial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
892
+ return (await ctx.bypassAutomation(retryInitial, jar)) || retryInitial;
893
+ }
894
+ return (await ctx.bypassAutomation(initial, jar)) || initial;
895
+ }
896
+ const hydrated = await hydrateJarFromDB(null);
897
+ if (hydrated) {
898
+ logger("🔄 AppState backup found — restoring session...", "info");
899
+ const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
900
+ return (await ctx.bypassAutomation(initial, jar)) || initial;
901
+ }
902
+ logger("🔑 AppState expired — logging in with credentials...", "warn");
903
+ return get("https://www.facebook.com/", null, null, globalOptions)
904
+ .then(saveCookies(jar))
905
+ .then(makeLogin(jar, email, password, globalOptions))
906
+ .then(function () {
907
+ return get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
908
+ });
909
+ })()
910
+ .then(async function (res) {
911
+ const ctx = {};
912
+ ctx.options = globalOptions;
913
+ ctx.bypassAutomation = async function (resp, j) {
914
+ global.fca = global.fca || {};
915
+ global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
916
+ const s = x => (typeof x === "string" ? x : String(x ?? ""));
917
+ const u = r => r?.request?.res?.responseUrl || (r?.config?.baseURL ? new URL(r.config.url || "/", r.config.baseURL).toString() : r?.config?.url || "");
918
+ const isCp = r => typeof u(r) === "string" && u(r).includes("checkpoint/601051028565049");
919
+ const cookieUID = async () => {
920
+ try {
921
+ const cookies = typeof j?.getCookies === "function" ? await j.getCookies("https://www.facebook.com") : [];
922
+ return cookies.find(c => c.key === "i_user")?.value || cookies.find(c => c.key === "c_user")?.value;
923
+ } catch { return undefined; }
924
+ };
925
+ const htmlUID = body => s(body).match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s(body).match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
926
+ const getUID = async body => (await cookieUID()) || htmlUID(body);
927
+ const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
928
+ const bypass = async body => {
929
+ const b = s(body);
930
+ const UID = await getUID(b);
931
+ const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
932
+ const jazoest = getFrom(b, 'name="jazoest" value="', '"') || getFrom(b, "jazoest=", '",') || b.match(/name="jazoest"\s+value="([^"]+)"/)?.[1];
933
+ const lsd = getFrom(b, '["LSD",[],{"token":"', '"}') || b.match(/name="lsd"\s+value="([^"]+)"/)?.[1];
934
+ const form = { av: UID, fb_dtsg, jazoest, lsd, fb_api_caller_class: "RelayModern", fb_api_req_friendly_name: "FBScrapingWarningMutation", variables: "{}", server_timestamps: true, doc_id: 6339492849481770 };
935
+ await post("https://www.facebook.com/api/graphql/", j, form, null, this.options).then(saveCookies(j));
936
+ logger("⚠️ Facebook automation detected — bypassing...", "warn");
937
+ };
938
+ try {
939
+ if (res && isCp(res)) {
940
+ await bypass(s(res.data));
941
+ const refreshed = await refreshJar();
942
+ if (!isCp(refreshed)) logger("✅ Bypass complete — session restored.", "info");
943
+ return refreshed;
944
+ }
945
+ logger("✅ No checkpoint detected — session is clean.", "info");
946
+ return res;
947
+ } catch {
948
+ return res;
949
+ }
950
+ };
951
+ const processed = (await ctx.bypassAutomation(res, jar)) || res;
952
+ let html = processed && processed.data ? processed.data : "";
953
+ let cookies = await Promise.resolve(jar.getCookies("https://www.facebook.com"));
954
+ const getUIDFromCookies = cs =>
955
+ cs.find(c => c.key === "i_user")?.value ||
956
+ cs.find(c => c.key === "c_user")?.value ||
957
+ cs.find(c => c.name === "i_user")?.value ||
958
+ cs.find(c => c.name === "c_user")?.value;
959
+ const getUIDFromHTML = body => {
960
+ const s = typeof body === "string" ? body : String(body ?? "");
961
+ return s.match(/"USER_ID"\s*:\s*"(\d+)"/)?.[1] || s.match(/\["CurrentUserInitialData",\[\],\{.*?"USER_ID":"(\d+)".*?\},\d+\]/)?.[1];
962
+ };
963
+ // Helper to validate UID - must be a non-zero positive number string
964
+ const isValidUID = uid => uid && uid !== "0" && /^\d+$/.test(uid) && parseInt(uid, 10) > 0;
965
+
966
+ let userID = getUIDFromCookies(cookies);
967
+ // Also try to extract userID from HTML if not found in cookies
968
+ if (!isValidUID(userID)) {
969
+ userID = getUIDFromHTML(html);
970
+ }
971
+ // If still not found and appState was provided, use userID from appState input as fallback
972
+ if (!isValidUID(userID) && userIDFromAppState && isValidUID(userIDFromAppState)) {
973
+ userID = userIDFromAppState;
974
+ }
975
+ // Trigger auto-login if userID is invalid (missing or "0")
976
+ if (!isValidUID(userID)) {
977
+ logger("Invalid userID detected (missing or 0), attempting auto-login...", "warn");
978
+ // Pass hadAppStateInput=true if appState/Cookie was originally provided
979
+ const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
980
+ html = retried.html;
981
+ cookies = retried.cookies;
982
+ userID = retried.userID;
983
+
984
+ // Validate HTML after auto-login - ensure it contains valid USER_ID
985
+ const htmlUserIDAfterLogin = getUIDFromHTML(html);
986
+ if (!isValidUID(htmlUserIDAfterLogin)) {
987
+ logger("After auto-login, HTML still does not contain valid USER_ID. Session may not be established.", "error");
988
+ // Try one more refresh
989
+ try {
990
+ const refreshRes = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
991
+ const refreshedHtml = refreshRes && refreshRes.data ? refreshRes.data : "";
992
+ const refreshedHtmlUID = getUIDFromHTML(refreshedHtml);
993
+ if (isValidUID(refreshedHtmlUID)) {
994
+ html = refreshedHtml;
995
+ userID = refreshedHtmlUID;
996
+ logger(`After refresh, found valid USER_ID in HTML: ${userID}`, "info");
997
+ } else {
998
+ throw new Error("Login failed - HTML does not show valid USER_ID after auto-login and refresh");
999
+ }
1000
+ } catch (refreshErr) {
1001
+ throw new Error(`Login failed - Could not establish valid session. HTML USER_ID check failed: ${refreshErr && refreshErr.message ? refreshErr.message : String(refreshErr)}`);
1002
+ }
1003
+ } else {
1004
+ // Use USER_ID from HTML as it's more reliable
1005
+ userID = htmlUserIDAfterLogin;
1006
+ logger(`After auto-login, using USER_ID from HTML: ${userID}`, "info");
1007
+ }
1008
+ }
1009
+ if (html.includes("/checkpoint/block/?next")) {
1010
+ logger("💀 Session expired! Please update your appstate.", "error");
1011
+ throw new Error("Checkpoint");
1012
+ }
1013
+
1014
+ // Final validation: ensure HTML shows we're logged in before proceeding
1015
+ let finalHtmlUID = getUIDFromHTML(html);
1016
+ if (!isValidUID(finalHtmlUID)) {
1017
+ // If cookies have valid UID but HTML doesn't, try to "activate" session
1018
+ if (isValidUID(userID)) {
1019
+ logger(`HTML shows USER_ID=${finalHtmlUID || "none"} but cookies have valid UID=${userID}. Attempting to activate session...`, "warn");
1020
+
1021
+ // Try making requests to activate the session
1022
+ try {
1023
+ // Wait a bit first for cookies to propagate
1024
+ await new Promise(resolve => setTimeout(resolve, 1000));
1025
+
1026
+ // Try refreshing with m.facebook.com/home.php (mobile home page)
1027
+ logger("Trying to activate session via m.facebook.com/home.php...", "info");
1028
+ const activateRes = await get("https://m.facebook.com/home.php", jar, null, globalOptions).then(saveCookies(jar));
1029
+ const activateHtml = activateRes && activateRes.data ? activateRes.data : "";
1030
+ const activateUID = getUIDFromHTML(activateHtml);
1031
+
1032
+ if (isValidUID(activateUID)) {
1033
+ html = activateHtml;
1034
+ finalHtmlUID = activateUID;
1035
+ userID = activateUID;
1036
+ logger(`Session activated! Found valid USER_ID in HTML: ${userID}`, "info");
1037
+ } else {
1038
+ // Try one more time with www.facebook.com/home.php after delay
1039
+ await new Promise(resolve => setTimeout(resolve, 1500));
1040
+ logger("Trying to activate session via www.facebook.com/home.php...", "info");
1041
+ const activateRes2 = await get("https://www.facebook.com/home.php", jar, null, globalOptions).then(saveCookies(jar));
1042
+ const activateHtml2 = activateRes2 && activateRes2.data ? activateRes2.data : "";
1043
+ const activateUID2 = getUIDFromHTML(activateHtml2);
1044
+
1045
+ if (isValidUID(activateUID2)) {
1046
+ html = activateHtml2;
1047
+ finalHtmlUID = activateUID2;
1048
+ userID = activateUID2;
1049
+ logger(`Session activated on second try! Found valid USER_ID in HTML: ${userID}`, "info");
1050
+ } else {
1051
+ // If cookies have valid UID, we can proceed with cookie-based UID but warn
1052
+ logger(`WARNING: HTML still shows USER_ID=${finalHtmlUID || "none"} but cookies have valid UID=${userID}. Proceeding with cookie-based UID.`, "warn");
1053
+ // Don't throw error, proceed with cookie-based UID
1054
+ }
1055
+ }
1056
+ } catch (activateErr) {
1057
+ logger(`Failed to activate session: ${activateErr && activateErr.message ? activateErr.message : String(activateErr)}. Proceeding with cookie-based UID.`, "warn");
1058
+ // Don't throw error, proceed with cookie-based UID
1059
+ }
1060
+ } else {
1061
+ // No valid UID in either cookies or HTML
1062
+ logger(`Final HTML validation failed - USER_ID from HTML: ${finalHtmlUID || "none"}, from cookies: ${userID || "none"}`, "error");
1063
+ throw new Error("Login validation failed - HTML does not contain valid USER_ID. Session may not be properly established.");
1064
+ }
1065
+ }
1066
+
1067
+ // Final check: ensure we have a valid userID (either from HTML or cookies)
1068
+ if (!isValidUID(userID)) {
1069
+ logger(`No valid USER_ID found - HTML: ${finalHtmlUID || "none"}, Cookies: ${userID || "none"}`, "error");
1070
+ throw new Error("Login validation failed - No valid USER_ID found in HTML or cookies.");
1071
+ }
1072
+ let mqttEndpoint;
1073
+ let region = "PRN";
1074
+ let fb_dtsg;
1075
+ let irisSeqID;
1076
+ try {
1077
+ const m1 = html.match(/"endpoint":"([^"]+)"/);
1078
+ const m2 = m1 ? null : html.match(/endpoint\\":\\"([^\\"]+)\\"/);
1079
+ const raw = (m1 && m1[1]) || (m2 && m2[1]);
1080
+ if (raw) mqttEndpoint = raw.replace(/\\\//g, "/");
1081
+ region = parseRegion(html);
1082
+ const rinfo = REGION_MAP.get(region);
1083
+ if (rinfo) logger(`Server region ${region} - ${rinfo.name}`, "info");
1084
+ else logger(`Server region ${region}`, "info");
1085
+ } catch {
1086
+ logger("Not MQTT endpoint", "warn");
1087
+ }
1088
+ try {
1089
+ const userDataMatch = String(html).match(/\["CurrentUserInitialData",\[\],({.*?}),\d+\]/);
1090
+ if (userDataMatch) {
1091
+ const info = JSON.parse(userDataMatch[1]);
1092
+ logger(`✅ Logged in as: ${info.NAME} (${info.USER_ID})`, "info");
1093
+
1094
+ // Check if Facebook response shows USER_ID = 0 (session dead)
1095
+ if (!isValidUID(info.USER_ID)) {
1096
+ logger("Facebook response shows invalid USER_ID (0 or empty), session is dead!", "warn");
1097
+ // Force trigger auto-login
1098
+ const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
1099
+ html = retried.html;
1100
+ cookies = retried.cookies;
1101
+ userID = retried.userID;
1102
+ // Re-check after auto-login
1103
+ if (!isValidUID(userID)) {
1104
+ throw new Error("Auto-login failed - could not get valid userID");
1105
+ }
1106
+ }
1107
+ } else if (userID) {
1108
+ logger(`ID người dùng: ${userID}`, "info");
1109
+ }
1110
+ } catch (userDataErr) {
1111
+ // If error is from our validation, rethrow it
1112
+ if (userDataErr && userDataErr.message && userDataErr.message.includes("Auto-login failed")) {
1113
+ throw userDataErr;
1114
+ }
1115
+ // Otherwise ignore parsing errors
1116
+ }
1117
+ const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
1118
+ if (tokenMatch) fb_dtsg = tokenMatch[1];
1119
+ try {
1120
+ if (userID) await backupAppStateSQL(jar, userID);
1121
+ } catch { }
1122
+ Promise.resolve()
1123
+ .then(function () {
1124
+ if (models && models.sequelize && typeof models.sequelize.authenticate === "function") {
1125
+ return models.sequelize.authenticate();
1126
+ }
1127
+ })
1128
+ .then(function () {
1129
+ if (models && typeof models.syncAll === "function") {
1130
+ return models.syncAll();
1131
+ }
1132
+ })
1133
+ .catch(function (error) {
1134
+ // Silently handle database errors - they're not critical for login
1135
+ const errorMsg = error && error.message ? error.message : String(error);
1136
+ if (!errorMsg.includes("No Sequelize instance passed")) {
1137
+ // Only log non-Sequelize instance errors
1138
+ logger(`Database connection failed: ${errorMsg}`, "warn");
1139
+ }
1140
+ });
1141
+ logger("⚡ fca-eryxenx | Powered by EryXenX | github.com/EryXenX", "info");
1142
+ const emitter = new EventEmitter();
1143
+ const ctxMain = {
1144
+ userID,
1145
+ jar,
1146
+ globalOptions,
1147
+ loggedIn: true,
1148
+ access_token: "NONE",
1149
+ clientMutationId: 0,
1150
+ mqttClient: undefined,
1151
+ lastSeqId: irisSeqID,
1152
+ syncToken: undefined,
1153
+ mqttEndpoint,
1154
+ region,
1155
+ firstListen: true,
1156
+ fb_dtsg,
1157
+ clientID: ((Math.random() * 2147483648) | 0).toString(16),
1158
+ clientId: getFrom(html, '["MqttWebDeviceID",[],{"clientID":"', '"}') || "",
1159
+ wsReqNumber: 0,
1160
+ wsTaskNumber: 0,
1161
+ tasks: new Map(),
1162
+ _emitter: emitter
1163
+ };
1164
+ ctxMain.options = globalOptions;
1165
+ ctxMain.bypassAutomation = ctx.bypassAutomation.bind(ctxMain);
1166
+ ctxMain.performAutoLogin = async () => {
1167
+ if (config.autoLogin !== true && config.autoLogin !== "true") return false;
1168
+ try {
1169
+ const u = config.credentials?.email || email;
1170
+ const p = config.credentials?.password || password;
1171
+ const tf = config.credentials?.twofactor || null;
1172
+ if (!u || !p) return false;
1173
+ const r = await tokens(u, p, tf);
1174
+ if (!(r && r.status && Array.isArray(r.cookies))) return false;
1175
+ const pairs = r.cookies.map(c => `${c.key || c.name}=${c.value}`);
1176
+ setJarFromPairs(jar, pairs, ".facebook.com");
1177
+ await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
1178
+ return true;
1179
+ } catch {
1180
+ return false;
1181
+ }
1182
+ };
1183
+ const api = {
1184
+ setOptions: require("./options").setOptions.bind(null, globalOptions),
1185
+ getCookies: function () {
1186
+ return cookieHeaderFromJar(jar);
1187
+ },
1188
+ getAppState: function () {
1189
+ return getAppState(jar);
1190
+ },
1191
+ getLatestAppStateFromDB: async function (uid = userID) {
1192
+ const data = await getLatestBackup(uid, "appstate");
1193
+ return data ? JSON.parse(data) : null;
1194
+ },
1195
+ getLatestCookieFromDB: async function (uid = userID) {
1196
+ return await getLatestBackup(uid, "cookie");
1197
+ },
1198
+ on: emitter.on.bind(emitter),
1199
+ once: emitter.once.bind(emitter),
1200
+ off: emitter.removeListener.bind(emitter),
1201
+ removeAllListeners: emitter.removeAllListeners.bind(emitter)
1202
+ };
1203
+ const defaultFuncs = makeDefaults(html, userID, ctxMain);
1204
+
1205
+ // Attach lightweight DB updaters for realtime events (MQTT)
1206
+ try {
1207
+ const Thread = models && models.Thread;
1208
+ if (!Thread) {
1209
+ throw new Error("Thread model not available");
1210
+ }
1211
+
1212
+ ctxMain._updateThreadFromMessage = async msg => {
1213
+ try {
1214
+ if (!msg || !msg.threadID) return;
1215
+ const id = String(msg.threadID);
1216
+ // Fast path: increment messageCount column directly
1217
+ let affected = 0;
1218
+ try {
1219
+ const res = await Thread.increment("messageCount", { by: 1, where: { threadID: id } });
1220
+ if (Array.isArray(res) && typeof res[0] === "number") {
1221
+ affected = res[0];
1222
+ }
1223
+ } catch {
1224
+ // Ignore increment errors, we'll try to create below
1225
+ }
1226
+ // If no row was updated, ensure a row exists
1227
+ if (!affected) {
1228
+ try {
1229
+ await Thread.create({ threadID: id, messageCount: 1, data: { threadID: id } });
1230
+ } catch {
1231
+ // Ignore create races / duplicates
1232
+ }
1233
+ }
1234
+ } catch (e) {
1235
+ const msgText = e && e.message ? e.message : String(e);
1236
+ logger(`updateThreadFromMessage error: ${msgText}`, "warn");
1237
+ }
1238
+ };
1239
+ } catch {
1240
+ // If DB layer is unavailable, skip realtime thread updates
1241
+ }
1242
+
1243
+ // Attach remote control client if enabled in config
1244
+ let remote = null;
1245
+ try {
1246
+ if (config && config.remoteControl && config.remoteControl.enabled) {
1247
+ remote = createRemoteClient(api, ctxMain, config.remoteControl);
1248
+ }
1249
+ } catch (e) {
1250
+ logger(`Remote control initialization failed: ${e && e.message ? e.message : String(e)}`, "warn");
1251
+ }
1252
+ if (remote) {
1253
+ api.remote = remote;
1254
+ }
1255
+ const srcRoot = path.join(__dirname, "../src/api");
1256
+ let loaded = 0;
1257
+ let skipped = 0;
1258
+ fs.readdirSync(srcRoot, { withFileTypes: true }).forEach((sub) => {
1259
+ if (!sub.isDirectory()) return;
1260
+ const subDir = path.join(srcRoot, sub.name);
1261
+ fs.readdirSync(subDir, { withFileTypes: true }).forEach((entry) => {
1262
+ if (!entry.isFile() || !entry.name.endsWith(".js")) return;
1263
+ const p = path.join(subDir, entry.name);
1264
+ const key = path.basename(entry.name, ".js");
1265
+ if (api[key]) {
1266
+ skipped++;
1267
+ return;
1268
+ }
1269
+ let mod;
1270
+ try {
1271
+ mod = require(p);
1272
+ } catch (e) {
1273
+ logger(`Failed to require API module ${p}: ${e && e.message ? e.message : String(e)}`, "warn");
1274
+ skipped++;
1275
+ return;
1276
+ }
1277
+ const factory = typeof mod === "function" ? mod : (mod && typeof mod.default === "function" ? mod.default : null);
1278
+ if (!factory) {
1279
+ logger(`API module ${p} does not export a function, skipping`, "warn");
1280
+ skipped++;
1281
+ return;
1282
+ }
1283
+ api[key] = factory(defaultFuncs, api, ctxMain);
1284
+ loaded++;
1285
+ });
1286
+ });
1287
+ logger(`Loaded ${loaded} FCA API methods${skipped ? `, skipped ${skipped} duplicates` : ""}`);
1288
+ if (api.listenMqtt) {
1289
+ api._listenMqttRaw = api.listenMqtt;
1290
+ api.listen = api.listenMqtt;
1291
+ }
1292
+ if (api.refreshFb_dtsg && !ctxMain._dtsgRefreshInterval) {
1293
+ ctxMain._dtsgRefreshInterval = setInterval(function () {
1294
+ api.refreshFb_dtsg().then(function () {
1295
+ logger("Successfully refreshed fb_dtsg");
1296
+ }).catch(function () {
1297
+ logger("An error occurred while refreshing fb_dtsg", "error");
1298
+ });
1299
+ }, 86400000);
1300
+ // Don't stack a new interval per login, and don't keep the process
1301
+ // alive just for this.
1302
+ if (ctxMain._dtsgRefreshInterval.unref) ctxMain._dtsgRefreshInterval.unref();
1303
+ }
1304
+ // ✅ nexca core features register
1305
+ try {
1306
+ const e2eeModule = require("../src/api/socket/e2ee");
1307
+ api.e2ee = new e2eeModule.E2EEBridge(ctxMain, api, defaultFuncs);
1308
+ ctxMain.e2ee = api.e2ee;
1309
+ api.connectE2EE = (deviceStorePath) => api.e2ee.connect(deviceStorePath, ctxMain.userID);
1310
+ api.listenE2EE = require("../src/api/socket/listenE2EE")(defaultFuncs, api, ctxMain);
1311
+
1312
+ // E2EE native connect() blocks the Node process while it runs
1313
+ // (koffi FFI call into the Go engine, not truly async despite the
1314
+ // Promise wrapper) — calling it here during login used to race
1315
+ // against the bot's own database load and stall bot startup for
1316
+ // minutes. It is no longer auto-connected here; the bot triggers
1317
+ // it explicitly once its own startup (DB load etc.) is finished
1318
+ // via api.connectE2EE().
1319
+ api._connectE2EEAndMerge = () => {
1320
+ logger("Connecting E2EE (this can take a little while)...", "warn");
1321
+ return api.connectE2EE()
1322
+ .then(() => {
1323
+ api.listen = api.listenE2EE;
1324
+ api.listenMqtt = api.listenE2EE;
1325
+ logger("E2EE connected and merged into api.listen()/api.listenMqtt()");
1326
+ })
1327
+ .catch((e) => {
1328
+ logger(`E2EE auto-connect failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1329
+ });
1330
+ };
1331
+ } catch (e) {
1332
+ logger(`E2EE init failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1333
+ }
1334
+
1335
+ try {
1336
+ api.sessionGuard = require("../src/api/messaging/sessionGuard")(defaultFuncs, api, ctxMain);
1337
+ } catch (e) {
1338
+ logger(`sessionGuard init failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1339
+ }
1340
+
1341
+ try {
1342
+ api.sendBroadcast = require("../src/api/messaging/sendBroadcast")(defaultFuncs, api, ctxMain);
1343
+ } catch (e) {
1344
+ logger(`sendBroadcast init failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1345
+ }
1346
+
1347
+ // sendMessage override with nexca version (better MQTT + HTTP fallback)
1348
+ try {
1349
+ const nexcaSendMsg = require("../src/api/socket/sendMessage")(defaultFuncs, api, ctxMain);
1350
+ api.sendMessage = nexcaSendMsg;
1351
+ api.sendMessageMqtt = require("../src/api/socket/sendMessageMqtt")(defaultFuncs, api, ctxMain);
1352
+ api.OldMessage = require("../src/api/socket/OldMessage")(defaultFuncs, api, ctxMain);
1353
+ api.sendMessageDM = (msg, threadID, cb, replyTo) => api.OldMessage(msg, threadID, cb, replyTo, true);
1354
+ } catch (e) {
1355
+ logger(`sendMessage nexca override failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1356
+ }
1357
+
1358
+ // Typing indicator (default ON, zero added latency): shows "typing..." as soon as a
1359
+ // command-like message arrives, clears it when the reply goes out. Must exist before
1360
+ // the listenMqtt override below because that is where its middleware is installed.
1361
+ try {
1362
+ const { createTypingIndicator, wrapSendMessage } = require("../src/utils/typingIndicator");
1363
+ const typing = createTypingIndicator(api, ctxMain, ctxMain.globalOptions && ctxMain.globalOptions.typingIndicator, logger);
1364
+ ctxMain._typing = typing;
1365
+ wrapSendMessage(api, typing);
1366
+ api.getTypingIndicator = () => typing;
1367
+ api.setTypingIndicator = (opt) => typing.configure(opt);
1368
+ api.withTyping = (threadID, fn) => typing.withTyping(threadID, fn);
1369
+ } catch (e) {
1370
+ logger(`typing indicator init failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1371
+ }
1372
+
1373
+ // listenMqtt override with nexca version (better MQTT stability)
1374
+ try {
1375
+ api.listenMqtt = require("../src/api/socket/listenMqtt")(defaultFuncs, api, ctxMain);
1376
+ api.listen = api.listenMqtt;
1377
+ // Refresh the "raw" MQTT reference listenE2EE.js uses internally.
1378
+ // It was captured earlier (right after base FCA methods loaded,
1379
+ // before this nexca override existed), so it was pointing at the
1380
+ // pre-humanizer listener. Once E2EE connects and swaps
1381
+ // api.listenMqtt to listenE2EE, that stale reference meant every
1382
+ // message routed through the MQTT side of E2EE mode skipped the
1383
+ // humanizer entirely — no delay, no typing, on literally every
1384
+ // message while E2EE was active.
1385
+ api._listenMqttRaw = api.listenMqtt;
1386
+ } catch (e) {
1387
+ logger(`listenMqtt nexca override failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn");
1388
+ }
1389
+
1390
+ try {
1391
+ // Version-update notice removed — it was comparing against
1392
+ // "fca-eryxenx" on the public npm registry, an unrelated
1393
+ // package (this fork ships as @eryxenx/fca), so it always
1394
+ // produced a bogus/misleading version diff.
1395
+ } catch (_) {}
1396
+ logger("🚀 Login successful! Bot is ready.");
1397
+ callback(null, api);
1398
+ })
1399
+ .catch(function (e) {
1400
+ callback(e);
1401
+ });
1402
+ } catch (e) {
1403
+ callback(e);
1404
+ }
1405
+ }
1406
+
1407
+ module.exports = loginHelper;
1408
+ module.exports.loginHelper = loginHelper;
1409
+ module.exports.tokensViaAPI = tokensViaAPI;
1410
+ module.exports.loginViaAPI = loginViaAPI;
1411
+ module.exports.tokens = tokens;
1412
+ module.exports.normalizeCookieHeaderString = normalizeCookieHeaderString;
1413
+ module.exports.setJarFromPairs = setJarFromPairs;