ayman-fca 1.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 (124) hide show
  1. package/README.md +81 -0
  2. package/func/checkUpdate.js +13 -0
  3. package/func/logAdapter.js +30 -0
  4. package/func/logger.js +66 -0
  5. package/index.d.ts +751 -0
  6. package/index.js +15 -0
  7. package/module/config.js +38 -0
  8. package/module/login.js +111 -0
  9. package/module/loginHelper.js +1296 -0
  10. package/module/options.js +37 -0
  11. package/package.json +78 -0
  12. package/src/api/action/addExternalModule.js +19 -0
  13. package/src/api/action/changeAvatar.js +137 -0
  14. package/src/api/action/changeBio.js +48 -0
  15. package/src/api/action/enableAutoSaveAppState.js +72 -0
  16. package/src/api/action/getCurrentUserID.js +11 -0
  17. package/src/api/action/handleFriendRequest.js +33 -0
  18. package/src/api/action/logout.js +76 -0
  19. package/src/api/action/refreshFb_dtsg.js +62 -0
  20. package/src/api/action/setPostReaction.js +106 -0
  21. package/src/api/action/story.js +118 -0
  22. package/src/api/action/unfriend.js +30 -0
  23. package/src/api/http/httpGet.js +28 -0
  24. package/src/api/http/httpPost.js +32 -0
  25. package/src/api/http/postFormData.js +23 -0
  26. package/src/api/messaging/J +1 -0
  27. package/src/api/messaging/addUserToGroup.js +70 -0
  28. package/src/api/messaging/changeAdminStatus.js +72 -0
  29. package/src/api/messaging/changeArchivedStatus.js +31 -0
  30. package/src/api/messaging/changeBlockedStatus.js +27 -0
  31. package/src/api/messaging/changeGroupImage.js +91 -0
  32. package/src/api/messaging/changeNickname.js +70 -0
  33. package/src/api/messaging/changeThreadColor.js +44 -0
  34. package/src/api/messaging/changeThreadEmoji.js +111 -0
  35. package/src/api/messaging/createNewGroup.js +50 -0
  36. package/src/api/messaging/createPoll.js +52 -0
  37. package/src/api/messaging/createThemeAI.js +98 -0
  38. package/src/api/messaging/deleteMessage.js +73 -0
  39. package/src/api/messaging/deleteThread.js +29 -0
  40. package/src/api/messaging/editMessage.js +67 -0
  41. package/src/api/messaging/forwardAttachment.js +55 -0
  42. package/src/api/messaging/forwardMessage.js +73 -0
  43. package/src/api/messaging/getEmojiUrl.js +29 -0
  44. package/src/api/messaging/getFriendsList.js +82 -0
  45. package/src/api/messaging/getMessage.js +829 -0
  46. package/src/api/messaging/getThemePictures.js +62 -0
  47. package/src/api/messaging/groupActions.js +119 -0
  48. package/src/api/messaging/handleMessageRequest.js +31 -0
  49. package/src/api/messaging/markAsDelivered.js +31 -0
  50. package/src/api/messaging/markAsRead.js +88 -0
  51. package/src/api/messaging/markAsReadAll.js +28 -0
  52. package/src/api/messaging/markAsSeen.js +30 -0
  53. package/src/api/messaging/muteThread.js +27 -0
  54. package/src/api/messaging/notes.js +101 -0
  55. package/src/api/messaging/removeUserFromGroup.js +51 -0
  56. package/src/api/messaging/resolvePhotoUrl.js +29 -0
  57. package/src/api/messaging/scheduler.js +100 -0
  58. package/src/api/messaging/searchForThread.js +32 -0
  59. package/src/api/messaging/sendMessage.js +270 -0
  60. package/src/api/messaging/sendTypingIndicator.js +62 -0
  61. package/src/api/messaging/setMessageReaction.js +91 -0
  62. package/src/api/messaging/setTitle.js +86 -0
  63. package/src/api/messaging/shareContact.js +47 -0
  64. package/src/api/messaging/threadColors.js +128 -0
  65. package/src/api/messaging/unsendMessage.js +73 -0
  66. package/src/api/messaging/uploadAttachment.js +492 -0
  67. package/src/api/socket/core/connectMqtt.js +259 -0
  68. package/src/api/socket/core/emitAuth.js +79 -0
  69. package/src/api/socket/core/getSeqID.js +170 -0
  70. package/src/api/socket/core/getTaskResponseData.js +27 -0
  71. package/src/api/socket/core/parseDelta.js +377 -0
  72. package/src/api/socket/detail/buildStream.js +215 -0
  73. package/src/api/socket/detail/constants.js +29 -0
  74. package/src/api/socket/listenMqtt.js +377 -0
  75. package/src/api/socket/middleware/index.js +80 -0
  76. package/src/api/threads/getThreadHistory.js +664 -0
  77. package/src/api/threads/getThreadInfo.js +296 -0
  78. package/src/api/threads/getThreadList.js +293 -0
  79. package/src/api/threads/getThreadPictures.js +43 -0
  80. package/src/api/user/J +1 -0
  81. package/src/api/user/getUserID.js +48 -0
  82. package/src/api/user/getUserInfo.js +402 -0
  83. package/src/api/user/getUserInfoV2.js +134 -0
  84. package/src/core/sendReqMqtt.js +69 -0
  85. package/src/database/helpers.js +36 -0
  86. package/src/database/models/index.js +55 -0
  87. package/src/database/models/thread.js +44 -0
  88. package/src/database/models/user.js +39 -0
  89. package/src/database/threadData.js +92 -0
  90. package/src/database/userData.js +88 -0
  91. package/src/remote/remoteClient.js +71 -0
  92. package/src/utils/broadcast.js +62 -0
  93. package/src/utils/client.js +10 -0
  94. package/src/utils/constants.js +53 -0
  95. package/src/utils/cookies.js +73 -0
  96. package/src/utils/format/attachment.js +357 -0
  97. package/src/utils/format/cookie.js +9 -0
  98. package/src/utils/format/date.js +50 -0
  99. package/src/utils/format/decode.js +44 -0
  100. package/src/utils/format/delta.js +194 -0
  101. package/src/utils/format/ids.js +64 -0
  102. package/src/utils/format/index.js +64 -0
  103. package/src/utils/format/message.js +88 -0
  104. package/src/utils/format/presence.js +132 -0
  105. package/src/utils/format/readTyp.js +44 -0
  106. package/src/utils/format/thread.js +42 -0
  107. package/src/utils/format/utils.js +141 -0
  108. package/src/utils/headers.js +96 -0
  109. package/src/utils/loginParser/autoLogin.js +125 -0
  110. package/src/utils/loginParser/helpers.js +43 -0
  111. package/src/utils/loginParser/index.js +10 -0
  112. package/src/utils/loginParser/parseAndCheckLogin.js +220 -0
  113. package/src/utils/loginParser/textUtils.js +28 -0
  114. package/src/utils/request/H +1 -0
  115. package/src/utils/request/client.js +33 -0
  116. package/src/utils/request/config.js +25 -0
  117. package/src/utils/request/defaults.js +40 -0
  118. package/src/utils/request/helpers.js +31 -0
  119. package/src/utils/request/index.js +12 -0
  120. package/src/utils/request/methods.js +92 -0
  121. package/src/utils/request/proxy.js +23 -0
  122. package/src/utils/request/retry.js +87 -0
  123. package/src/utils/request/sanitize.js +41 -0
  124. package/src/utils/sessionKeeper.js +275 -0
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+
3
+ module.exports = function(_defaultFuncs, _api, _ctx) {
4
+ // Currently the only colors that can be passed to api.changeThreadColor(); may change if Facebook adds more
5
+ return {
6
+ //Old hex colors.
7
+ ////MessengerBlue: null,
8
+ ////Viking: "#44bec7",
9
+ ////GoldenPoppy: "#ffc300",
10
+ ////RadicalRed: "#fa3c4c",
11
+ ////Shocking: "#d696bb",
12
+ ////PictonBlue: "#6699cc",
13
+ ////FreeSpeechGreen: "#13cf13",
14
+ ////Pumpkin: "#ff7e29",
15
+ ////LightCoral: "#e68585",
16
+ ////MediumSlateBlue: "#7646ff",
17
+ ////DeepSkyBlue: "#20cef5",
18
+ ////Fern: "#67b868",
19
+ ////Cameo: "#d4a88c",
20
+ ////BrilliantRose: "#ff5ca1",
21
+ ////BilobaFlower: "#a695c7"
22
+
23
+ //#region This part is for backward compatibly
24
+ //trying to match the color one-by-one. kill me plz
25
+ MessengerBlue: "196241301102133", //DefaultBlue
26
+ Viking: "1928399724138152", //TealBlue
27
+ GoldenPoppy: "174636906462322", //Yellow
28
+ RadicalRed: "2129984390566328", //Red
29
+ Shocking: "2058653964378557", //LavenderPurple
30
+ FreeSpeechGreen: "2136751179887052", //Green
31
+ Pumpkin: "175615189761153", //Orange
32
+ LightCoral: "980963458735625", //CoralPink
33
+ MediumSlateBlue: "234137870477637", //BrightPurple
34
+ DeepSkyBlue: "2442142322678320", //AquaBlue
35
+ BrilliantRose: "169463077092846", //HotPink
36
+ DefaultBlue: "196241301102133",
37
+ HotPink: "169463077092846",
38
+ AquaBlue: "2442142322678320",
39
+ BrightPurple: "234137870477637",
40
+ CoralPink: "980963458735625",
41
+ Orange: "175615189761153",
42
+ Green: "2136751179887052",
43
+ LavenderPurple: "2058653964378557",
44
+ Red: "2129984390566328",
45
+ Yellow: "174636906462322",
46
+ TealBlue: "1928399724138152",
47
+ Aqua: "417639218648241",
48
+ Mango: "930060997172551",
49
+ Berry: "164535220883264",
50
+ Citrus: "370940413392601",
51
+ Candy: "205488546921017",
52
+
53
+ /**
54
+ * July 06, 2022
55
+ * added by @NTKhang
56
+ */
57
+ Earth: "1833559466821043",
58
+ Support: "365557122117011",
59
+ Music: "339021464972092",
60
+ Pride: "1652456634878319",
61
+ DoctorStrange: "538280997628317",
62
+ LoFi: "1060619084701625",
63
+ Sky: "3190514984517598",
64
+ LunarNewYear: "357833546030778",
65
+ Celebration: "627144732056021",
66
+ Chill: "390127158985345",
67
+ StrangerThings: "1059859811490132",
68
+ Dune: "1455149831518874",
69
+ Care: "275041734441112",
70
+ Astrology: "3082966625307060",
71
+ JBalvin: "184305226956268",
72
+ Birthday: "621630955405500",
73
+ Cottagecore: "539927563794799",
74
+ Ocean: "736591620215564",
75
+ Love: "741311439775765",
76
+ TieDye: "230032715012014",
77
+ Monochrome: "788274591712841",
78
+ Default: "3259963564026002",
79
+ Rocket: "582065306070020",
80
+ Berry2: "724096885023603",
81
+ Candy2: "624266884847972",
82
+ Unicorn: "273728810607574",
83
+ Tropical: "262191918210707",
84
+ Maple: "2533652183614000",
85
+ Sushi: "909695489504566",
86
+ Citrus2: "557344741607350",
87
+ Lollipop: "280333826736184",
88
+ Shadow: "271607034185782",
89
+ Rose: "1257453361255152",
90
+ Lavender: "571193503540759",
91
+ Tulip: "2873642949430623",
92
+ Classic: "3273938616164733",
93
+ Peach: "3022526817824329",
94
+ Honey: "672058580051520",
95
+ Kiwi: "3151463484918004",
96
+ Grape: "193497045377796",
97
+
98
+ /**
99
+ * July 15, 2022
100
+ * added by @NTKhang
101
+ */
102
+ NonBinary: "737761000603635",
103
+
104
+ /**
105
+ * November 25, 2022
106
+ * added by @NTKhang
107
+ */
108
+ ThankfulForFriends: "1318983195536293",
109
+ Transgender: "504518465021637",
110
+ TaylorSwift: "769129927636836",
111
+ NationalComingOutDay: "788102625833584",
112
+ Autumn: "822549609168155",
113
+ Cyberpunk2077: "780962576430091",
114
+
115
+ /**
116
+ * May 13, 2023
117
+ */
118
+ MothersDay: "1288506208402340",
119
+ APAHM: "121771470870245",
120
+ Parenthood: "810978360551741",
121
+ StarWars: "1438011086532622",
122
+ GuardianOfTheGalaxy: "101275642962533",
123
+ Bloom: "158263147151440",
124
+ BubbleTea: "195296273246380",
125
+ Basketball: "6026716157422736",
126
+ ElephantsAndFlowers: "693996545771691"
127
+ };
128
+ };
@@ -0,0 +1,73 @@
1
+ // ============================================================
2
+ // AYMAN-FCA v2.0 — Unsend Message
3
+ // © 2025 Ayman. All Rights Reserved.
4
+ // ============================================================
5
+ "use strict";
6
+
7
+ const { generateOfflineThreadingID } = require("../../utils/format");
8
+ const log = require("../../../func/logAdapter");
9
+
10
+ module.exports = function(defaultFuncs, api, ctx) {
11
+ return function unsendMessage(messageID, threadID, callback) {
12
+ return new Promise((resolve, reject) => {
13
+ if (!ctx.mqttClient) {
14
+ const err = new Error("AYMAN-FCA: MQTT غير متصل");
15
+ callback?.(err); return reject(err);
16
+ }
17
+
18
+ const reqID = ++ctx.wsReqNumber;
19
+ const taskID = ++ctx.wsTaskNumber;
20
+
21
+ const content = {
22
+ app_id: "2220391788200892",
23
+ payload: JSON.stringify({
24
+ tasks: [{
25
+ failure_count: null,
26
+ label: "33",
27
+ payload: JSON.stringify({ message_id: messageID, thread_key: threadID, sync_group: 1 }),
28
+ queue_name: "unsend_message",
29
+ task_id: taskID
30
+ }],
31
+ epoch_id: parseInt(generateOfflineThreadingID()),
32
+ version_id: "25393437286970779"
33
+ }),
34
+ request_id: reqID,
35
+ type: 3
36
+ };
37
+
38
+ let done = false;
39
+ const timer = setTimeout(() => {
40
+ if (done) return;
41
+ done = true;
42
+ ctx.mqttClient?.removeListener("message", handleRes);
43
+ callback?.(null, { success: true });
44
+ resolve({ success: true });
45
+ }, 10000);
46
+
47
+ const handleRes = (topic, message) => {
48
+ if (topic !== "/ls_resp") return;
49
+ let msg;
50
+ try { msg = JSON.parse(message.toString()); msg.payload = JSON.parse(msg.payload); } catch { return; }
51
+ if (msg.request_id !== reqID) return;
52
+ if (done) return;
53
+ done = true;
54
+ clearTimeout(timer);
55
+ ctx.mqttClient?.removeListener("message", handleRes);
56
+ callback?.(null, { success: true });
57
+ resolve({ success: true });
58
+ };
59
+
60
+ ctx.mqttClient.on("message", handleRes);
61
+ ctx.mqttClient.publish("/ls_req", JSON.stringify(content), { qos: 1, retain: false }, err => {
62
+ if (err) {
63
+ if (done) return;
64
+ done = true;
65
+ clearTimeout(timer);
66
+ ctx.mqttClient?.removeListener("message", handleRes);
67
+ log.error("unsendMessage", err);
68
+ callback?.(err); reject(err);
69
+ }
70
+ });
71
+ });
72
+ };
73
+ };
@@ -0,0 +1,492 @@
1
+ "use strict";
2
+
3
+ const axios = require("axios");
4
+ const { wrapper } = require("axios-cookiejar-support");
5
+ const { CookieJar } = require("tough-cookie");
6
+ const FormData = require("form-data");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const stream = require("stream");
10
+ const { URL } = require("url");
11
+ const log = require("../../../func/logAdapter");
12
+
13
+ let http = null;
14
+ let cookieJar = new CookieJar();
15
+ let tokenCache = null;
16
+ let tokenCacheTime = 0;
17
+ const TOKEN_CACHE_TTL = 5 * 60 * 1000;
18
+
19
+ const DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
20
+
21
+ function cleanJSON(x) {
22
+ if (typeof x !== "string") return x;
23
+ const s = x.replace(/^for\s*\(;;\);\s*/i, "");
24
+ try { return JSON.parse(s); } catch { return s; }
25
+ }
26
+
27
+ function pick(re, html, i = 1) {
28
+ const m = html && html.match(re);
29
+ return m ? m[i] : "";
30
+ }
31
+
32
+ function getFrom(html, a, b) {
33
+ const i = html.indexOf(a);
34
+ if (i < 0) return;
35
+ const start = i + a.length;
36
+ const j = html.indexOf(b, start);
37
+ return j < 0 ? undefined : html.slice(start, j);
38
+ }
39
+
40
+ function respFinalUrl(res) {
41
+ return (res && (res.url || res.requestUrl)) || "";
42
+ }
43
+
44
+ function detectCheckpoint(res) {
45
+ const url = String(respFinalUrl(res) || "");
46
+ const body = typeof res?.body === "string" ? res.body : "";
47
+ const hit =
48
+ /\/checkpoint\//i.test(url) ||
49
+ /(?:href|action)\s*=\s*["']https?:\/\/[^"']*\/checkpoint\//i.test(body) ||
50
+ /"checkpoint"|checkpoint_title|checkpointMain|id="checkpoint"/i.test(body) ||
51
+ (/login\.php/i.test(url) && /checkpoint/i.test(body));
52
+ return { hit, url: url || (body.match(/https?:\/\/[^"']*\/checkpoint\/[^"'<>]*/i)?.[0] || "") };
53
+ }
54
+
55
+ function checkpointError(res) {
56
+ const d = detectCheckpoint(res);
57
+ if (!d.hit) return null;
58
+ const e = new Error("Checkpoint required");
59
+ e.code = "CHECKPOINT";
60
+ e.checkpoint = true;
61
+ e.url = d.url || "https://www.facebook.com/checkpoint/";
62
+ e.status = res?.statusCode || res?.status;
63
+ return e;
64
+ }
65
+
66
+ async function httpGet(pageUrl, ua, headers = {}) {
67
+ const host = new URL(pageUrl).hostname;
68
+ const referer = `https://${host}/`;
69
+ const baseHeaders = {
70
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
71
+ "Accept-Language": "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7",
72
+ "Accept-Encoding": "gzip, deflate, br",
73
+ "Cache-Control": "max-age=0",
74
+ Connection: "keep-alive",
75
+ Host: host,
76
+ Origin: `https://${host}`,
77
+ Referer: referer,
78
+ "Sec-Ch-Prefers-Color-Scheme": "dark",
79
+ "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
80
+ "Sec-Ch-Ua-Full-Version-List": '"Google Chrome";v="143.0.7499.182", "Chromium";v="143.0.7499.182", "Not A(Brand";v="24.0.0.0"',
81
+ "Sec-Ch-Ua-Mobile": "?0",
82
+ "Sec-Ch-Ua-Model": '""',
83
+ "Sec-Ch-Ua-Platform": '"Windows"',
84
+ "Sec-Ch-Ua-Platform-Version": '"19.0.0"',
85
+ "Sec-Fetch-Dest": "document",
86
+ "Sec-Fetch-Mode": "navigate",
87
+ "Sec-Fetch-Site": "same-origin",
88
+ "Sec-Fetch-User": "?1",
89
+ "Upgrade-Insecure-Requests": "1",
90
+ "User-Agent": ua || DEFAULT_UA,
91
+ "x-fb-rlafr": "0"
92
+ };
93
+ const res = await http.get(pageUrl, {
94
+ headers: { ...baseHeaders, ...headers },
95
+ timeout: 30000
96
+ });
97
+ const cp = checkpointError(res);
98
+ if (cp) throw cp;
99
+ return typeof res.data === "string" ? res.data : String(res.data || "");
100
+ }
101
+
102
+ async function getTokens(ua, forceRefresh = false) {
103
+ const now = Date.now();
104
+ if (!forceRefresh && tokenCache && (now - tokenCacheTime) < TOKEN_CACHE_TTL) {
105
+ return tokenCache;
106
+ }
107
+ try {
108
+ const html = await httpGet("https://www.facebook.com/", ua, { Referer: "https://www.facebook.com/" });
109
+ const fb_dtsg = getFrom(html, '"DTSGInitData",[],{"token":"', '",') || html.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1] || "";
110
+ const jazoest = getFrom(html, 'name="jazoest" value="', '"') || getFrom(html, "jazoest=", '",') || html.match(/name="jazoest"\s+value="([^"]+)"/)?.[1] || "";
111
+ const lsd = getFrom(html, '["LSD",[],{"token":"', '"}') || html.match(/name="lsd"\s+value="([^"]+)"/)?.[1] || "";
112
+ const spin_r = pick(/"__spin_r":(\d+)/, html) || "";
113
+ const spin_t = pick(/"__spin_t":(\d+)/, html) || "";
114
+ const rev = pick(/"__rev":(\d+)/, html) || "";
115
+
116
+ if (!fb_dtsg || !lsd) {
117
+ // Cố gắng fallback nếu regex fail, nhưng thường là do cookie die
118
+ if (!tokenCache) throw new Error("Failed to fetch fb_dtsg or LSD from Facebook");
119
+ }
120
+
121
+ tokenCache = { lsd, fb_dtsg, jazoest, spin_r, spin_t, rev };
122
+ tokenCacheTime = now;
123
+ return tokenCache;
124
+ } catch (e) {
125
+ if (tokenCache) {
126
+ log.warn("[uploadAttachment] Token fetch failed, using cached tokens: " + (e.message || e));
127
+ return tokenCache;
128
+ }
129
+ throw e;
130
+ }
131
+ }
132
+
133
+ function getType(obj) { return Object.prototype.toString.call(obj).slice(8, -1); }
134
+ function isReadableStream(obj) { return obj instanceof stream.Readable && (getType(obj._read) === "Function" || getType(obj._read) === "AsyncFunction") && getType(obj._readableState) === "Object"; }
135
+ function fromBuffer(buf) { return stream.Readable.from(buf); }
136
+ function parseDataUrl(s) { const m = /^data:([^;,]+)?(;base64)?,(.*)$/i.exec(s); if (!m) return null; const mime = m[1] || "application/octet-stream"; const isB64 = !!m[2]; const data = isB64 ? Buffer.from(m[3], "base64") : Buffer.from(decodeURIComponent(m[3]), "utf8"); return { mime, data }; }
137
+
138
+ function filenameFromUrl(u, headers) {
139
+ try {
140
+ const urlObj = new URL(u);
141
+ let filename = path.basename(urlObj.pathname) || `file-${Date.now()}`;
142
+ const cd = headers && (headers["content-disposition"] || headers["Content-Disposition"]);
143
+ if (cd) {
144
+ const m = /filename\*?=(?:UTF-8''|")?([^";\n]+)/i.exec(cd);
145
+ if (m) filename = decodeURIComponent(m[1].replace(/"/g, ""));
146
+ }
147
+ return filename;
148
+ } catch {
149
+ return `file-${Date.now()}`;
150
+ }
151
+ }
152
+
153
+ async function normalizeOne(input, ua) {
154
+ if (!input) throw new Error("Invalid input");
155
+ if (Buffer.isBuffer(input)) return { stream: fromBuffer(input), filename: `file-${Date.now()}.bin`, contentType: "application/octet-stream" };
156
+ if (typeof input === "string") {
157
+ if (/^https?:\/\//i.test(input)) {
158
+ const resp = await http.get(input, {
159
+ headers: {
160
+ "User-Agent": ua,
161
+ Accept: "*/*",
162
+ "Accept-Encoding": "gzip, deflate, br",
163
+ "Cache-Control": "no-cache"
164
+ },
165
+ timeout: 30000,
166
+ responseType: "stream"
167
+ });
168
+ const s = resp.data;
169
+ const filename = filenameFromUrl(input, resp.headers);
170
+ return { stream: s, filename };
171
+ }
172
+ if (input.startsWith("data:")) {
173
+ const p = parseDataUrl(input);
174
+ if (!p) throw new Error("Bad data URL");
175
+ return { stream: fromBuffer(p.data), filename: `file-${Date.now()}`, contentType: p.mime };
176
+ }
177
+ if (fs.existsSync(input) && fs.statSync(input).isFile()) {
178
+ return { stream: fs.createReadStream(input), filename: path.basename(input) };
179
+ }
180
+ throw new Error(`Unsupported string input: ${input}`);
181
+ }
182
+ if (isReadableStream(input)) {
183
+ return { stream: input, filename: `file-${Date.now()}` };
184
+ }
185
+ if (typeof input === "object") {
186
+ if (input.buffer && Buffer.isBuffer(input.buffer)) {
187
+ const filename = input.filename || `file-${Date.now()}.bin`;
188
+ const contentType = input.contentType || "application/octet-stream";
189
+ return { stream: fromBuffer(input.buffer), filename, contentType };
190
+ }
191
+ if (input.data && Buffer.isBuffer(input.data)) {
192
+ const filename = input.filename || `file-${Date.now()}.bin`;
193
+ const contentType = input.contentType || "application/octet-stream";
194
+ return { stream: fromBuffer(input.data), filename, contentType };
195
+ }
196
+ if (input.stream && isReadableStream(input.stream)) {
197
+ const filename = input.filename || `file-${Date.now()}`;
198
+ const contentType = input.contentType;
199
+ return { stream: input.stream, filename, contentType };
200
+ }
201
+ if (input.url) {
202
+ return normalizeOne(String(input.url), ua);
203
+ }
204
+ if (input.path && fs.existsSync(input.path) && fs.statSync(input.path).isFile()) {
205
+ return { stream: fs.createReadStream(input.path), filename: input.filename || path.basename(input.path), contentType: input.contentType };
206
+ }
207
+ }
208
+ throw new Error("Unrecognized input");
209
+ }
210
+
211
+ function mapAttachmentDetails(data) {
212
+ const out = [];
213
+ if (!data || typeof data !== "object") return out;
214
+
215
+ const stack = [data];
216
+ while (stack.length) {
217
+ const cur = stack.pop();
218
+ if (!cur || typeof cur !== "object") continue;
219
+ const id = cur.video_id || cur.image_id || cur.audio_id || cur.file_id || cur.fbid || cur.id || cur.upload_id || cur.gif_id;
220
+ const idKey =
221
+ cur.video_id ? "video_id" :
222
+ cur.image_id ? "image_id" :
223
+ cur.audio_id ? "audio_id" :
224
+ cur.file_id ? "file_id" :
225
+ cur.gif_id ? "gif_id" :
226
+ cur.fbid ? "fbid" :
227
+ id ? "id" : null;
228
+ const filename = cur.filename || cur.file_name || cur.name || cur.original_filename;
229
+ const filetype = cur.filetype || cur.mime_type || cur.type || cur.content_type;
230
+ let thumbnail = cur.thumbnail_src || cur.thumbnail_url || cur.preview_url || cur.thumbSrc || cur.thumb_url || cur.image_preview_url || cur.large_preview_url;
231
+ if (!thumbnail) {
232
+ const m = cur.media || cur.thumbnail || cur.thumb || cur.image_data || cur.video_data || cur.preview;
233
+ thumbnail = m?.thumbnail_src || m?.thumbnail_url || m?.src || m?.uri || m?.url;
234
+ }
235
+ if (idKey) {
236
+ const o = {};
237
+ o[idKey] = id;
238
+ if (filename) o.filename = filename;
239
+ if (filetype) o.filetype = filetype;
240
+ if (thumbnail) o.thumbnail_src = thumbnail;
241
+ out.push(o);
242
+ }
243
+ if (Array.isArray(cur)) {
244
+ for (const v of cur) stack.push(v);
245
+ } else {
246
+ for (const k of Object.keys(cur)) stack.push(cur[k]);
247
+ }
248
+ }
249
+
250
+ if (!out.length && data.payload && Array.isArray(data.payload.metadata)) {
251
+ return data.payload.metadata.slice();
252
+ }
253
+
254
+ return out;
255
+ }
256
+
257
+ function pLimit(n) {
258
+ let active = 0;
259
+ const queue = [];
260
+ const next = () => {
261
+ active--;
262
+ if (queue.length) queue.shift()();
263
+ };
264
+ return fn => new Promise((resolve, reject) => {
265
+ const run = () => {
266
+ active++;
267
+ fn().then(v => { resolve(v); next(); }).catch(e => { reject(e); next(); });
268
+ };
269
+ if (active < n) run(); else queue.push(run);
270
+ });
271
+ }
272
+
273
+ // Hàm upload core xử lý request
274
+ async function singleUpload(urlBase, file, ua, tokens, retries = 2) {
275
+ const form = new FormData();
276
+ // QUAN TRỌNG: Chỉ append file, KHÔNG append fb_dtsg vào body nữa
277
+ form.append("farr", file.stream, { filename: file.filename, contentType: file.contentType });
278
+
279
+ const headers = {
280
+ ...form.getHeaders(),
281
+ Accept: "*/*",
282
+ "Accept-Language": "vi,en-US;q=0.9,en;q=0.8,fr-FR;q=0.7,fr;q=0.6",
283
+ "Accept-Encoding": "gzip, deflate, br",
284
+ "User-Agent": ua,
285
+ "x-asbd-id": "359341",
286
+ "x-fb-lsd": tokens.lsd || "",
287
+ "x-fb-friendly-name": "MercuryUpload",
288
+ "x-fb-request-analytics-tags": JSON.stringify({
289
+ network_tags: {
290
+ product: "256002347743983",
291
+ purpose: "none",
292
+ request_category: "graphql",
293
+ retry_attempt: "0"
294
+ },
295
+ application_tags: "graphservice"
296
+ }),
297
+ "sec-ch-prefers-color-scheme": "dark",
298
+ "sec-ch-ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"',
299
+ "sec-ch-ua-mobile": "?0",
300
+ "sec-ch-ua-platform": '"Windows"',
301
+ "sec-fetch-dest": "empty",
302
+ "sec-fetch-mode": "cors",
303
+ "sec-fetch-site": "same-origin",
304
+ Origin: "https://www.facebook.com",
305
+ Referer: "https://www.facebook.com/",
306
+ "x-fb-rlafr": "0",
307
+ Connection: "keep-alive"
308
+ };
309
+
310
+ // Build URL với query string tokens
311
+ const finalUrl = new URL(urlBase);
312
+ finalUrl.searchParams.set("fb_dtsg", tokens.fb_dtsg);
313
+ finalUrl.searchParams.set("jazoest", tokens.jazoest);
314
+ finalUrl.searchParams.set("lsd", tokens.lsd);
315
+ finalUrl.searchParams.set("__aaid", "0");
316
+ finalUrl.searchParams.set("__ccg", "EXCELLENT");
317
+
318
+ for (let attempt = 0; attempt <= retries; attempt++) {
319
+ try {
320
+ const res = await http.post(finalUrl.toString(), form, {
321
+ headers,
322
+ timeout: 120000,
323
+ maxContentLength: Infinity,
324
+ maxBodyLength: Infinity
325
+ });
326
+ return res;
327
+ } catch (e) {
328
+ if (attempt === retries) throw e;
329
+ if (e.code === "ETIMEDOUT" || e.code === "ECONNRESET" || (e.response && e.response.status >= 500)) {
330
+ await new Promise(r => setTimeout(r, (attempt + 1) * 1000));
331
+ continue;
332
+ }
333
+ throw e;
334
+ }
335
+ }
336
+ }
337
+
338
+ module.exports = function (defaultFuncs, api, ctx) {
339
+ const ua = ctx?.options?.userAgent || DEFAULT_UA;
340
+ cookieJar = ctx.jar instanceof CookieJar ? ctx.jar : new CookieJar();
341
+
342
+ // Axios instance
343
+ http = wrapper(axios.create({
344
+ timeout: 60000,
345
+ headers: {
346
+ "User-Agent": ua,
347
+ "Accept-Language": "vi-VN,vi;q=0.9,en-US;q=0.8,en;q=0.7",
348
+ "Accept-Encoding": "gzip, deflate, br",
349
+ Connection: "keep-alive"
350
+ },
351
+ maxRedirects: 5,
352
+ validateStatus: () => true
353
+ }));
354
+ http.defaults.withCredentials = true;
355
+ http.defaults.jar = cookieJar;
356
+
357
+ async function uploadCore(link, opts, callback) {
358
+ if (typeof opts === "function") { callback = opts; opts = undefined; }
359
+ const options = {
360
+ concurrency: Math.max(1, Math.min(5, Number(opts?.concurrency || 3))),
361
+ mode: opts?.mode === "single" ? "single" : "parallel"
362
+ };
363
+
364
+ let resolveFunc = function () { };
365
+ let rejectFunc = function () { };
366
+ const returnPromise = new Promise(function (resolve, reject) {
367
+ resolveFunc = resolve;
368
+ rejectFunc = reject;
369
+ });
370
+ if (!callback) {
371
+ callback = function (err, data) {
372
+ if (err) return rejectFunc(err);
373
+ resolveFunc(data);
374
+ };
375
+ }
376
+
377
+ (async () => {
378
+ try {
379
+ const inputsArr = Array.isArray(link) ? link : [link];
380
+ if (!inputsArr.length) {
381
+ const e = new Error("No files to upload");
382
+ callback(e);
383
+ return;
384
+ }
385
+
386
+ let tokens = await getTokens(ua);
387
+ const normAll = await Promise.all(inputsArr.map(x => normalizeOne(x, ua)));
388
+
389
+ // Base QS setup
390
+ const qs = [];
391
+ const userId = (ctx && (ctx.userID || ctx.userId)) ? String(ctx.userID || ctx.userId) : "";
392
+ if (userId) qs.push(`__user=${encodeURIComponent(userId)}`);
393
+ qs.push("__a=1");
394
+ qs.push("dpr=1");
395
+ const reqId = Math.floor(Math.random() * 36 ** 2).toString(36);
396
+ qs.push(`__req=${encodeURIComponent(reqId)}`);
397
+ if (tokens.spin_r) qs.push(`__spin_r=${encodeURIComponent(tokens.spin_r)}`);
398
+ if (tokens.spin_t) qs.push(`__spin_t=${encodeURIComponent(tokens.spin_t)}`);
399
+ if (tokens.rev) qs.push(`__rev=${encodeURIComponent(tokens.rev)}`);
400
+ qs.push("__spin_b=trunk");
401
+ qs.push("__comet_req=15");
402
+
403
+ const baseUrl = `https://www.facebook.com/ajax/mercury/upload.php?${qs.join("&")}`;
404
+
405
+ if (options.mode === "single") {
406
+ const f = normAll[0];
407
+ const res = await singleUpload(baseUrl, f, ua, tokens);
408
+
409
+ const cp = checkpointError(res);
410
+ if (cp) { tokenCache = null; throw cp; }
411
+
412
+ const data = cleanJSON(res.data);
413
+ const ids = mapAttachmentDetails(data);
414
+
415
+ if (!ids.length) {
416
+ const e = new Error("UploadFb returned no metadata/ids");
417
+ e.code = "NO_METADATA";
418
+ e.status = res.status;
419
+ e.body = typeof data === "string" ? data.slice(0, 500) : data;
420
+ throw e;
421
+ }
422
+ log.info(`[uploadAttachment] success ${ids.length} item(s) status ${res.status}`);
423
+ callback(null, { status: res.status, ids, raw: data });
424
+ return;
425
+ }
426
+
427
+ // Parallel mode
428
+ const limit = pLimit(options.concurrency);
429
+ const tasks = normAll.map(f => () => singleUpload(baseUrl, f, ua, tokens));
430
+ const results = await Promise.all(tasks.map(t => limit(t)));
431
+
432
+ const ids = [];
433
+ const errors = [];
434
+ for (let i = 0; i < results.length; i++) {
435
+ const res = results[i];
436
+ try {
437
+ const cp = checkpointError(res);
438
+ if (cp) { tokenCache = null; throw cp; }
439
+
440
+ const data = cleanJSON(res.data);
441
+ const fileIds = mapAttachmentDetails(data);
442
+ if (!fileIds.length) {
443
+ log.warn(`[uploadAttachment] File ${i + 1} returned no metadata/ids`);
444
+ continue;
445
+ }
446
+ ids.push(...fileIds);
447
+ } catch (e) {
448
+ errors.push({ index: i, error: e });
449
+ log.error(`[uploadAttachment] Upload ${i + 1} failed: ${e.message || e}`);
450
+ }
451
+ }
452
+
453
+ if (ids.length === 0 && errors.length > 0) {
454
+ throw errors[0].error;
455
+ }
456
+
457
+ log.info(`[uploadAttachment] success ${ids.length}/${normAll.length} item(s)`);
458
+ callback(null, { status: 200, ids, raw: null, errors: errors.length > 0 ? errors : undefined });
459
+ } catch (e) {
460
+ if (e.code === "CHECKPOINT" || (e.response && [401, 403].includes(e.response.status))) {
461
+ tokenCache = null;
462
+ try {
463
+ await getTokens(ua, true);
464
+ log.info("[uploadAttachment] Tokens refreshed after error");
465
+ } catch (refreshErr) {
466
+ log.error("[uploadAttachment] Token refresh failed: " + (refreshErr.message || refreshErr));
467
+ }
468
+ }
469
+ log.error(`[uploadAttachment] error ${e.code || e.status || ""} ${e.message || e}`);
470
+ callback(e);
471
+ }
472
+ })().catch(err => {
473
+ log.error("[uploadAttachment] Unhandled promise rejection: " + (err.message || err));
474
+ rejectFunc(err);
475
+ });
476
+
477
+ return returnPromise;
478
+ }
479
+
480
+ return function uploadAttachment(attachments, callback) {
481
+ if (!attachments) throw { error: "Please pass an attachment or an array of attachments." };
482
+
483
+ if (typeof callback === "function") {
484
+ return uploadCore(attachments, { mode: "parallel" }, (err, result) => {
485
+ if (err) return callback(err);
486
+ callback(null, result && Array.isArray(result.ids) ? result.ids : []);
487
+ }).then(result => (result && Array.isArray(result.ids) ? result.ids : []));
488
+ }
489
+
490
+ return uploadCore(attachments, { mode: "parallel" }).then(result => result && Array.isArray(result.ids) ? result.ids : []);
491
+ };
492
+ };