@lazyneoaz/metachat 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/apis/changeAdminStatus.js +2 -6
- package/src/apis/createPoll.js +1 -1
- package/src/apis/getMessage.js +83 -29
- package/src/apis/getThemeInfo.js +6 -2
- package/src/apis/getThreadHistory.js +1 -1
- package/src/apis/getThreadInfo.js +15 -5
- package/src/apis/getThreadList.js +11 -5
- package/src/apis/listenMqtt.js +19 -40
- package/src/apis/markAsRead.js +12 -15
- package/src/apis/mqttDeltaValue.js +2 -2
- package/src/apis/nickname.js +1 -1
- package/src/apis/pinMessage.js +7 -5
- package/src/apis/removeUserFromGroup.js +1 -0
- package/src/apis/setMessageReaction.js +12 -11
- package/src/apis/setThreadTheme.js +11 -11
- package/src/apis/unsendMessage.js +1 -1
- package/src/utils/axios.js +30 -16
- package/src/utils/clients.js +112 -112
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"types": "src/types/index.d.ts",
|
|
6
6
|
"description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const utils = require('../utils');
|
|
4
4
|
|
|
5
|
-
function generateOfflineThreadingID() {
|
|
6
|
-
return Date.now().toString() + Math.floor(Math.random() * 1000000).toString();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
function getType(obj) {
|
|
10
6
|
return Object.prototype.toString.call(obj).slice(8, -1);
|
|
11
7
|
}
|
|
@@ -39,7 +35,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
39
35
|
if (ctx.mqttClient) {
|
|
40
36
|
const tasks = [];
|
|
41
37
|
const isAdmin = adminStatus ? 1 : 0;
|
|
42
|
-
const epochID = generateOfflineThreadingID();
|
|
38
|
+
const epochID = utils.generateOfflineThreadingID();
|
|
43
39
|
|
|
44
40
|
if (getType(adminID) === "Array") {
|
|
45
41
|
adminID.forEach((id, index) => {
|
|
@@ -93,7 +89,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
93
89
|
} else {
|
|
94
90
|
utils.warn("MQTT client not available, using HTTP fallback for changeAdminStatus");
|
|
95
91
|
const tasks = [];
|
|
96
|
-
const epochID = generateOfflineThreadingID();
|
|
92
|
+
const epochID = utils.generateOfflineThreadingID();
|
|
97
93
|
|
|
98
94
|
if (getType(adminID) === "Array") {
|
|
99
95
|
adminID.forEach((id, index) => {
|
package/src/apis/createPoll.js
CHANGED
|
@@ -61,7 +61,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
61
61
|
type: 3
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
ctx.mqttClient.publish("/ls_req", form);
|
|
64
|
+
ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false });
|
|
65
65
|
callback(null, { success: true });
|
|
66
66
|
} catch (err) {
|
|
67
67
|
utils.error("createPoll", err);
|
package/src/apis/getMessage.js
CHANGED
|
@@ -4,26 +4,72 @@ const utils = require('../utils');
|
|
|
4
4
|
const { _formatAttachment } = require('../utils/formatters/data/formatAttachment');
|
|
5
5
|
|
|
6
6
|
const THEME_COLORS = [
|
|
7
|
-
|
|
8
|
-
{ theme_color: "
|
|
9
|
-
{ theme_color: "
|
|
10
|
-
{ theme_color: "
|
|
11
|
-
{ theme_color: "
|
|
12
|
-
{ theme_color: "
|
|
13
|
-
{ theme_color: "
|
|
14
|
-
{ theme_color: "
|
|
15
|
-
{ theme_color: "
|
|
16
|
-
{ theme_color: "
|
|
17
|
-
{ theme_color: "
|
|
18
|
-
{ theme_color: "FF7646FF", theme_id: "
|
|
19
|
-
{ theme_color: "
|
|
20
|
-
{ theme_color: "
|
|
21
|
-
|
|
22
|
-
{ theme_color: "
|
|
23
|
-
{ theme_color: "
|
|
24
|
-
{ theme_color: "
|
|
25
|
-
{ theme_color: "
|
|
26
|
-
{ theme_color: "
|
|
7
|
+
// ── Core solid colours ────────────────────────────────────────────────────
|
|
8
|
+
{ theme_color: "FF0084FF", theme_id: "196241301102133", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Default Blue" },
|
|
9
|
+
{ theme_color: "FF0099FF", theme_id: "3273938616164733", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Classic" },
|
|
10
|
+
{ theme_color: "FF44BEC7", theme_id: "1928399724138152", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Teal Blue" },
|
|
11
|
+
{ theme_color: "FFFFC300", theme_id: "174636906462322", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Yellow" },
|
|
12
|
+
{ theme_color: "FFFA3C4C", theme_id: "2129984390566328", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Red" },
|
|
13
|
+
{ theme_color: "FF7646FF", theme_id: "234137870477637", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Bright Purple" },
|
|
14
|
+
{ theme_color: "FF13CF13", theme_id: "2136751179887052", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Green" },
|
|
15
|
+
{ theme_color: "FFFF7E29", theme_id: "175615189761153", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Orange" },
|
|
16
|
+
{ theme_color: "FFFF5CA1", theme_id: "169463077092846", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Hot Pink" },
|
|
17
|
+
{ theme_color: "FF25D366", theme_id: "2442142322678320", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Deep Sky Blue" },
|
|
18
|
+
{ theme_color: "FF7646FF", theme_id: "2058653964378557", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Lavender Purple" },
|
|
19
|
+
{ theme_color: "FFFF4500", theme_id: "980963458735625", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Light Coral" },
|
|
20
|
+
{ theme_color: "FF00C9C9", theme_id: "417639218648241", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Aqua" },
|
|
21
|
+
// ── Named / gradient themes ───────────────────────────────────────────────
|
|
22
|
+
{ theme_color: "FF000000", theme_id: "788274591712841", theme_emoji: "🖤", gradient: '["FFF0F0F0"]', should_show_icon: "", theme_name_with_subtitle: "Monochrome" },
|
|
23
|
+
{ theme_color: "FF2825B5", theme_id: "271607034185782", theme_emoji: null, gradient: '["FF5E007E","FF331290","FF2825B5"]', should_show_icon: "1", theme_name_with_subtitle: "Shadow" },
|
|
24
|
+
{ theme_color: "FFD9A900", theme_id: "2533652183614000", theme_emoji: null, gradient: '["FF550029","FFAA3232","FFD9A900"]', should_show_icon: "1", theme_name_with_subtitle: "Maple" },
|
|
25
|
+
{ theme_color: "FFFB45DE", theme_id: "2873642949430623", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Tulip" },
|
|
26
|
+
{ theme_color: "FF5E007E", theme_id: "193497045377796", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Grape" },
|
|
27
|
+
{ theme_color: "FF7AA286", theme_id: "1455149831518874", theme_emoji: "🌑", gradient: '["FF25C0E1","FFCE832A"]', should_show_icon: "", theme_name_with_subtitle: "Dune" },
|
|
28
|
+
{ theme_color: "FFFAAF00", theme_id: "672058580051520", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Honey" },
|
|
29
|
+
{ theme_color: "FFF25C54", theme_id: "3022526817824329", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Peach" },
|
|
30
|
+
{ theme_color: "FFF01D6A", theme_id: "724096885023603", theme_emoji: null, gradient: '["FF005FFF","FF9200FF","FFFF2E19"]', should_show_icon: "1", theme_name_with_subtitle: "Berry" },
|
|
31
|
+
{ theme_color: "FFFF7CA8", theme_id: "624266884847972", theme_emoji: null, gradient: '["FFFF8FB2","FFA797FF","FF00E5FF"]', should_show_icon: "1", theme_name_with_subtitle: "Candy" },
|
|
32
|
+
{ theme_color: "FF930099", theme_id: "930060997172551", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Mango" },
|
|
33
|
+
{ theme_color: "FF4267B2", theme_id: "164535220883264", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Berry" },
|
|
34
|
+
{ theme_color: "FF00C400", theme_id: "370940413392601", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Citrus" },
|
|
35
|
+
{ theme_color: "FF50C878", theme_id: "557344741607350", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Citrus 2" },
|
|
36
|
+
{ theme_color: "FFFF0000", theme_id: "205488546921017", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Candy" },
|
|
37
|
+
{ theme_color: "FF8B4513", theme_id: "1833559466821043", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Earth" },
|
|
38
|
+
{ theme_color: "FF0084FF", theme_id: "365557122117011", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Support" },
|
|
39
|
+
{ theme_color: "FFFF6B6B", theme_id: "339021464972092", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Music" },
|
|
40
|
+
{ theme_color: "FFFF69B4", theme_id: "1652456634878319", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Pride" },
|
|
41
|
+
{ theme_color: "FF8B0000", theme_id: "538280997628317", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Doctor Strange" },
|
|
42
|
+
{ theme_color: "FF6C63FF", theme_id: "1060619084701625", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Lo-Fi" },
|
|
43
|
+
{ theme_color: "FF87CEEB", theme_id: "3190514984517598", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Sky" },
|
|
44
|
+
{ theme_color: "FFFF4500", theme_id: "357833546030778", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Lunar New Year" },
|
|
45
|
+
{ theme_color: "FFFF6347", theme_id: "627144732056021", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Celebration" },
|
|
46
|
+
{ theme_color: "FF4682B4", theme_id: "390127158985345", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Chill" },
|
|
47
|
+
{ theme_color: "FFFF0000", theme_id: "1059859811490132", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Stranger Things" },
|
|
48
|
+
{ theme_color: "FFD4A574", theme_id: "275041734441112", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Care" },
|
|
49
|
+
{ theme_color: "FF9B59B6", theme_id: "3082966625307060", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Astrology" },
|
|
50
|
+
{ theme_color: "FFFF8C00", theme_id: "184305226956268", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "J Balvin" },
|
|
51
|
+
{ theme_color: "FFFF69B4", theme_id: "621630955405500", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Birthday" },
|
|
52
|
+
{ theme_color: "FF228B22", theme_id: "539927563794799", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Cottagecore" },
|
|
53
|
+
{ theme_color: "FF006994", theme_id: "736591620215564", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Ocean" },
|
|
54
|
+
{ theme_color: "FFFF1493", theme_id: "741311439775765", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Love" },
|
|
55
|
+
{ theme_color: "FFFF7F7F", theme_id: "230032715012014", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Tie Dye" },
|
|
56
|
+
{ theme_color: "FF808080", theme_id: "262191918210707", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Tropical" },
|
|
57
|
+
{ theme_color: "FF228B22", theme_id: "909695489504566", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Sushi" },
|
|
58
|
+
{ theme_color: "FFFF69B4", theme_id: "280333826736184", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Lollipop" },
|
|
59
|
+
{ theme_color: "FFFF007F", theme_id: "1257453361255152", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Rose" },
|
|
60
|
+
{ theme_color: "FFE6E6FA", theme_id: "571193503540759", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Lavender" },
|
|
61
|
+
{ theme_color: "FFFFC0CB", theme_id: "3151463484918004", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Kiwi" },
|
|
62
|
+
{ theme_color: "FF6F2DA8", theme_id: "810978360551741", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Parenthood" },
|
|
63
|
+
{ theme_color: "FF4169E1", theme_id: "1438011086532622", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Star Wars" },
|
|
64
|
+
{ theme_color: "FF6B8E23", theme_id: "101275642962533", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Guardians of the Galaxy" },
|
|
65
|
+
{ theme_color: "FFFF69B4", theme_id: "158263147151440", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Bloom" },
|
|
66
|
+
{ theme_color: "FF9B59B6", theme_id: "195296273246380", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Bubble Tea" },
|
|
67
|
+
{ theme_color: "FFFF8C00", theme_id: "6026716157422736", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Basketball" },
|
|
68
|
+
{ theme_color: "FF4B0082", theme_id: "737761000603635", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Non-Binary" },
|
|
69
|
+
{ theme_color: "FF55CDFC", theme_id: "504518465021637", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Transgender" },
|
|
70
|
+
{ theme_color: "FFFC0080", theme_id: "769129927636836", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Taylor Swift" },
|
|
71
|
+
{ theme_color: "FFFF7700", theme_id: "822549609168155", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Autumn" },
|
|
72
|
+
{ theme_color: "FFFF007F", theme_id: "693996545771691", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Elephants and Flowers" },
|
|
27
73
|
];
|
|
28
74
|
|
|
29
75
|
function formatMessage(threadID, data) {
|
|
@@ -63,20 +109,28 @@ function formatMessage(threadID, data) {
|
|
|
63
109
|
const adminType = data.extensible_message_admin_text_type;
|
|
64
110
|
|
|
65
111
|
if (adminType === "CHANGE_THREAD_THEME") {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
112
|
+
const ext = data.extensible_message_admin_text || {};
|
|
113
|
+
const themeColor = ext.theme_color || null;
|
|
114
|
+
// Prefer the theme_id field returned directly by GraphQL.
|
|
115
|
+
// Fall back to a reverse lookup in THEME_COLORS for older API
|
|
116
|
+
// responses that omit theme_id.
|
|
117
|
+
const directThemeId = ext.theme_id || ext.theme_fbid || null;
|
|
118
|
+
const colorMatch = directThemeId
|
|
119
|
+
? THEME_COLORS.find(c => c.theme_id === directThemeId)
|
|
120
|
+
: (themeColor ? THEME_COLORS.find(c => c.theme_color === themeColor) : null);
|
|
121
|
+
|
|
69
122
|
return {
|
|
70
123
|
...baseMessage,
|
|
71
124
|
type: "event",
|
|
72
125
|
logMessageType: "log:thread-color",
|
|
73
|
-
logMessageData:
|
|
126
|
+
logMessageData: {
|
|
74
127
|
theme_color: themeColor,
|
|
75
|
-
theme_id: null,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
128
|
+
theme_id: directThemeId || (colorMatch ? colorMatch.theme_id : null),
|
|
129
|
+
theme_fbid: directThemeId || (colorMatch ? colorMatch.theme_id : null),
|
|
130
|
+
theme_emoji: ext.theme_emoji || (colorMatch ? colorMatch.theme_emoji : null),
|
|
131
|
+
gradient: ext.gradient || (colorMatch ? colorMatch.gradient : null),
|
|
132
|
+
should_show_icon: ext.should_show_icon != null ? ext.should_show_icon : (colorMatch ? colorMatch.should_show_icon : null),
|
|
133
|
+
theme_name_with_subtitle: ext.theme_name_with_subtitle || (colorMatch ? colorMatch.theme_name_with_subtitle : null)
|
|
80
134
|
},
|
|
81
135
|
logMessageBody: data.snippet
|
|
82
136
|
};
|
package/src/apis/getThemeInfo.js
CHANGED
|
@@ -84,15 +84,19 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
84
84
|
|
|
85
85
|
const info = Array.isArray(threadInfo) ? threadInfo[0] : threadInfo;
|
|
86
86
|
|
|
87
|
+
// theme_id comes from threadTheme.id (extracted at getThreadInfo level)
|
|
88
|
+
const themeId = info.theme_id || (info.threadTheme && info.threadTheme.id) || info.themeID || null;
|
|
87
89
|
const themeInfo = {
|
|
88
90
|
threadID: identifier,
|
|
89
91
|
threadName: info.threadName || info.name || '',
|
|
90
92
|
color: info.color || null,
|
|
91
93
|
emoji: info.emoji || '👍',
|
|
92
|
-
theme_id:
|
|
94
|
+
theme_id: themeId,
|
|
95
|
+
theme_fbid: themeId,
|
|
93
96
|
theme_color: info.theme_color || info.color || null,
|
|
94
97
|
gradient_colors: info.gradient_colors || null,
|
|
95
|
-
|
|
98
|
+
threadTheme: info.threadTheme || null,
|
|
99
|
+
is_default: !info.color && !themeId
|
|
96
100
|
};
|
|
97
101
|
|
|
98
102
|
if (callback) {
|
|
@@ -112,12 +112,22 @@ function formatThreadGraphQLResponse(data) {
|
|
|
112
112
|
emoji: messageThread.customization_info
|
|
113
113
|
? messageThread.customization_info.emoji
|
|
114
114
|
: null,
|
|
115
|
-
color:
|
|
116
|
-
messageThread.customization_info &&
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
color: (function() {
|
|
116
|
+
const raw = messageThread.customization_info &&
|
|
117
|
+
messageThread.customization_info.outgoing_bubble_color;
|
|
118
|
+
if (!raw) return null;
|
|
119
|
+
const s = String(raw);
|
|
120
|
+
// Format is FFRRGGBB (8 hex chars, ARGB). Strip the FF alpha prefix.
|
|
121
|
+
// Validate before slicing to avoid returning garbage for unexpected formats.
|
|
122
|
+
if (/^[0-9a-fA-F]{8}$/.test(s)) return s.slice(2);
|
|
123
|
+
// Handle #RRGGBB or #AARRGGBB
|
|
124
|
+
if (/^#[0-9a-fA-F]{6}$/.test(s)) return s.slice(1);
|
|
125
|
+
if (/^#[0-9a-fA-F]{8}$/.test(s)) return s.slice(3);
|
|
126
|
+
// Fallback: return as-is
|
|
127
|
+
return s;
|
|
128
|
+
})(),
|
|
120
129
|
threadTheme: messageThread.thread_theme,
|
|
130
|
+
theme_id: messageThread.thread_theme ? (messageThread.thread_theme.id || null) : null,
|
|
121
131
|
nicknames:
|
|
122
132
|
messageThread.customization_info &&
|
|
123
133
|
messageThread.customization_info.participant_customizations
|
|
@@ -102,12 +102,18 @@ function formatThreadGraphQLResponse(messageThread) {
|
|
|
102
102
|
emoji: messageThread.customization_info
|
|
103
103
|
? messageThread.customization_info.emoji
|
|
104
104
|
: null,
|
|
105
|
-
color:
|
|
106
|
-
messageThread.customization_info &&
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
color: (function() {
|
|
106
|
+
const raw = messageThread.customization_info &&
|
|
107
|
+
messageThread.customization_info.outgoing_bubble_color;
|
|
108
|
+
if (!raw) return null;
|
|
109
|
+
const s = String(raw);
|
|
110
|
+
if (/^[0-9a-fA-F]{8}$/.test(s)) return s.slice(2);
|
|
111
|
+
if (/^#[0-9a-fA-F]{6}$/.test(s)) return s.slice(1);
|
|
112
|
+
if (/^#[0-9a-fA-F]{8}$/.test(s)) return s.slice(3);
|
|
113
|
+
return s;
|
|
114
|
+
})(),
|
|
110
115
|
threadTheme: messageThread.thread_theme,
|
|
116
|
+
theme_id: messageThread.thread_theme ? (messageThread.thread_theme.id || null) : null,
|
|
111
117
|
nicknames:
|
|
112
118
|
messageThread.customization_info &&
|
|
113
119
|
messageThread.customization_info.participant_customizations
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -449,15 +449,17 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
451
|
} else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
452
|
+
if (ctx.globalOptions.listenTyping) {
|
|
453
|
+
const typ = {
|
|
454
|
+
type: "typ",
|
|
455
|
+
isTyping: !!jsonMessage.state,
|
|
456
|
+
from: jsonMessage.sender_fbid.toString(),
|
|
457
|
+
threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
|
|
458
|
+
};
|
|
459
|
+
globalCallback(null, typ);
|
|
460
|
+
}
|
|
459
461
|
} else if (topic === "/orca_presence") {
|
|
460
|
-
if (
|
|
462
|
+
if (ctx.globalOptions.updatePresence && jsonMessage.list) {
|
|
461
463
|
for (const data of jsonMessage.list) {
|
|
462
464
|
globalCallback(null, {
|
|
463
465
|
type: "presence",
|
|
@@ -808,39 +810,16 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
808
810
|
} catch (err) {
|
|
809
811
|
const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
|
|
810
812
|
const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (isNotLoggedIn || isLoginBlocked) {
|
|
816
|
-
const reason = isLoginBlocked ? "login_blocked" : "not_logged_in";
|
|
817
|
-
utils.error("MQTT", `Auth error in getSeqID: ${reason} — attempting auto re-login`);
|
|
818
|
-
|
|
819
|
-
// Mirror the close-handler re-login pattern: try handleSessionExpiry first.
|
|
820
|
-
// If it succeeds we schedule a reconnect; only fall back to emitAuthError
|
|
821
|
-
// (which kills listening) when re-login is unavailable or already in progress.
|
|
822
|
-
if (!ctx._mqttReauthing && globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
823
|
-
ctx._mqttReauthing = true;
|
|
824
|
-
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', msg)
|
|
825
|
-
.then((ok) => {
|
|
826
|
-
ctx._mqttReauthing = false;
|
|
827
|
-
if (ok && ctx.globalOptions.autoReconnect) {
|
|
828
|
-
ctx._reconnectAttempts = 0;
|
|
829
|
-
scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
|
|
830
|
-
} else if (!ok) {
|
|
831
|
-
emitAuthError(reason, msg);
|
|
832
|
-
}
|
|
833
|
-
})
|
|
834
|
-
.catch(() => {
|
|
835
|
-
ctx._mqttReauthing = false;
|
|
836
|
-
emitAuthError(reason, msg);
|
|
837
|
-
});
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return emitAuthError(reason, msg);
|
|
813
|
+
|
|
814
|
+
if (/Not logged in/i.test(msg)) {
|
|
815
|
+
utils.error("MQTT", "Auth error in getSeqID: Not logged in");
|
|
816
|
+
return emitAuthError("not_logged_in", msg);
|
|
842
817
|
}
|
|
843
|
-
|
|
818
|
+
if (/blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir/i.test(msg)) {
|
|
819
|
+
utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked");
|
|
820
|
+
return emitAuthError("login_blocked", msg);
|
|
821
|
+
}
|
|
822
|
+
|
|
844
823
|
utils.error("MQTT", "getSeqID error:", msg);
|
|
845
824
|
if (ctx.globalOptions.autoReconnect) {
|
|
846
825
|
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
package/src/apis/markAsRead.js
CHANGED
|
@@ -50,23 +50,20 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
50
50
|
|
|
51
51
|
callback(null);
|
|
52
52
|
} else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
53
|
+
const form = {
|
|
54
|
+
["ids[" + threadID + "]"]: read,
|
|
55
|
+
watermarkTimestamp: Date.now(),
|
|
56
|
+
shouldSendReadReceipt: true,
|
|
57
|
+
};
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
ctx.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{ qos: 1, retain: false },
|
|
64
|
-
r,
|
|
65
|
-
)
|
|
66
|
-
);
|
|
59
|
+
const resData = await defaultFuncs
|
|
60
|
+
.post("https://www.facebook.com/ajax/mercury/change_read_status.php", ctx.jar, form)
|
|
61
|
+
.then(utils.saveCookies(ctx.jar))
|
|
62
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
67
63
|
|
|
68
|
-
if (
|
|
69
|
-
|
|
64
|
+
if (resData && resData.error) {
|
|
65
|
+
const err = new Error(String(resData.error.message || resData.error));
|
|
66
|
+
callback(err);
|
|
70
67
|
return returnPromise;
|
|
71
68
|
}
|
|
72
69
|
|
|
@@ -183,7 +183,7 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
|
|
|
183
183
|
} catch (err) {
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
|
-
if (!ctx.globalOptions.
|
|
186
|
+
if (!ctx.globalOptions.selfListenEvent && fmtEvent2 && fmtEvent2.author && fmtEvent2.author.toString() === ctx.userID) return;
|
|
187
187
|
if (!ctx.loggedIn) return;
|
|
188
188
|
if (fmtEvent2) globalCallback(null, fmtEvent2);
|
|
189
189
|
break;
|
|
@@ -193,7 +193,7 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
|
|
|
193
193
|
var tid = v.delta.threadKey.threadFbId;
|
|
194
194
|
if (mid && tid) {
|
|
195
195
|
var form = {
|
|
196
|
-
av: ctx.globalOptions.pageID,
|
|
196
|
+
av: ctx.globalOptions.pageID || ctx.userID,
|
|
197
197
|
queries: JSON.stringify({
|
|
198
198
|
o0: {
|
|
199
199
|
doc_id: "2848441488556444",
|
package/src/apis/nickname.js
CHANGED
package/src/apis/pinMessage.js
CHANGED
|
@@ -34,11 +34,13 @@ function extractAndSearchLightspeedRequest(allJsonData, options = {}) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
if (outputFile) {
|
|
38
|
+
try {
|
|
39
|
+
fs.writeFileSync(outputFile, JSON.stringify(lightReq, null, 2), "utf8");
|
|
40
|
+
utils.log(`pin.js: Saved lightspeed_web_request to ${outputFile}`);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
utils.error("pin.js: Failed to write lightspeed_web_request.json", err);
|
|
43
|
+
}
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
|
|
@@ -67,6 +67,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
67
67
|
}
|
|
68
68
|
if (jsonMsg.request_id !== reqID) return;
|
|
69
69
|
responseHandled = true;
|
|
70
|
+
clearTimeout(timeout);
|
|
70
71
|
ctx.mqttClient.removeListener("message", handleRes);
|
|
71
72
|
callback(null, { success: true });
|
|
72
73
|
resolveFunc({ success: true });
|
|
@@ -5,20 +5,21 @@ const utils = require('../utils');
|
|
|
5
5
|
|
|
6
6
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
7
7
|
return async (reaction, messageID) => {
|
|
8
|
-
if (
|
|
9
|
-
const
|
|
8
|
+
if (reaction === undefined || reaction === null) throw new Error("Please enter a valid emoji.");
|
|
9
|
+
const action = reaction === "" ? "REMOVE_REACTION" : "ADD_REACTION";
|
|
10
|
+
const defData = await defaultFuncs.postFormData("https://www.facebook.com/webgraphql/mutation/", ctx.jar, {
|
|
10
11
|
doc_id: "1491398900900362",
|
|
11
12
|
variables: JSON.stringify({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
data: {
|
|
14
|
+
client_mutation_id: ctx.clientMutationId++,
|
|
15
|
+
actor_id: ctx.userID,
|
|
16
|
+
action,
|
|
17
|
+
message_id: messageID,
|
|
18
|
+
reaction
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
20
21
|
dpr: 1
|
|
21
|
-
});
|
|
22
|
+
}, {});
|
|
22
23
|
const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
|
|
23
24
|
if (!resData) {
|
|
24
25
|
throw new Error("setMessageReactionLegacy returned empty object.");
|
|
@@ -35,17 +35,17 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
35
35
|
|
|
36
36
|
// ── Hardcoded colour aliases (fast path — no API round trip needed) ──────
|
|
37
37
|
const PALETTE = {
|
|
38
|
-
blue: "196241301102133",
|
|
39
|
-
purple: "
|
|
40
|
-
green: "
|
|
41
|
-
pink: "
|
|
42
|
-
orange: "175615189761153",
|
|
43
|
-
red: "
|
|
44
|
-
yellow: "
|
|
45
|
-
teal: "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
default: "196241301102133",
|
|
38
|
+
blue: "196241301102133", // DefaultBlue / MessengerBlue
|
|
39
|
+
purple: "234137870477637", // BrightPurple / MediumSlateBlue
|
|
40
|
+
green: "2136751179887052", // FreeSpeechGreen / Green
|
|
41
|
+
pink: "169463077092846", // HotPink / BrilliantRose
|
|
42
|
+
orange: "175615189761153", // Pumpkin / Orange
|
|
43
|
+
red: "2129984390566328", // RadicalRed / Red
|
|
44
|
+
yellow: "174636906462322", // GoldenPoppy / Yellow
|
|
45
|
+
teal: "1928399724138152", // TealBlue / Viking
|
|
46
|
+
aqua: "417639218648241", // Aqua
|
|
47
|
+
black: "271607034185782", // Shadow (darkest solid theme)
|
|
48
|
+
default: "196241301102133", // DefaultBlue
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
(async function worker() {
|
|
@@ -10,7 +10,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
10
10
|
})
|
|
11
11
|
const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
|
|
12
12
|
if (resData.error) {
|
|
13
|
-
throw new Error(resData);
|
|
13
|
+
throw new Error(JSON.stringify(resData));
|
|
14
14
|
}
|
|
15
15
|
return resData;
|
|
16
16
|
};
|
package/src/utils/axios.js
CHANGED
|
@@ -77,22 +77,29 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (isScrapingWarning) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
80
|
+
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
81
|
+
ctx._emitter.emit('checkpoint', { type: 'scraping_warning', res: body });
|
|
82
|
+
}
|
|
83
|
+
// Attempt auto-login recovery — throwing immediately here escalates
|
|
84
|
+
// the checkpoint into a permanent ban without giving the session a
|
|
85
|
+
// chance to recover.
|
|
86
|
+
if (!ctx.auto_login && typeof ctx.performAutoLogin === 'function') {
|
|
87
|
+
ctx.auto_login = true;
|
|
85
88
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
89
|
+
const ok = await ctx.performAutoLogin();
|
|
90
|
+
if (ok) {
|
|
91
|
+
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
92
|
+
ctx._emitter.emit('autoLoginSuccess', { res: body });
|
|
93
|
+
}
|
|
94
|
+
ctx.auto_login = false;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
88
97
|
} catch (_) {}
|
|
98
|
+
ctx.auto_login = false;
|
|
89
99
|
}
|
|
90
100
|
const err = new Error('Facebook scraping warning checkpoint detected.');
|
|
91
101
|
err.error = 'checkpoint_scraping';
|
|
92
102
|
err.res = body;
|
|
93
|
-
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
94
|
-
ctx._emitter.emit('checkpoint', { type: 'scraping_warning', res: body });
|
|
95
|
-
}
|
|
96
103
|
throw err;
|
|
97
104
|
}
|
|
98
105
|
|
|
@@ -101,14 +108,10 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
101
108
|
// actual login page — NOT generic strings like '"login.php?"' or
|
|
102
109
|
// '"login_page"' which Facebook embeds in authenticated page payloads as
|
|
103
110
|
// navigation links and client-side route names, causing false positives.
|
|
104
|
-
// `/checkpoint/block/?next` is the URL Facebook redirects to when it forces
|
|
105
|
-
// a logout; it appears in the response body as a redirect target and is a
|
|
106
|
-
// reliable signal that the session is gone.
|
|
107
111
|
const isLoginRedirect =
|
|
108
112
|
bodyStr.includes('<form id="login_form"') ||
|
|
109
113
|
bodyStr.includes('id="loginbutton"') ||
|
|
110
|
-
bodyStr.includes('id="email" name="email"')
|
|
111
|
-
bodyStr.includes('/checkpoint/block/?next');
|
|
114
|
+
bodyStr.includes('id="email" name="email"');
|
|
112
115
|
|
|
113
116
|
const isLoginBlocked =
|
|
114
117
|
typeof body === 'object' && body !== null && body.error === 1357001;
|
|
@@ -261,6 +264,17 @@ async function requestWithRetry(requestFunction, retries = 5, endpoint = '', thr
|
|
|
261
264
|
if (error.response) {
|
|
262
265
|
const adapted = adaptResponse(error.response);
|
|
263
266
|
checkAndApplyRateLimitCooldowns(adapted.body);
|
|
267
|
+
|
|
268
|
+
// Emit rateLimit event on HTTP 429 so consumers can react
|
|
269
|
+
if (error.response.status === 429) {
|
|
270
|
+
if (ctx && ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
271
|
+
try { ctx._emitter.emit('rateLimit', { res: adapted.body, status: 429 }); } catch (_) {}
|
|
272
|
+
}
|
|
273
|
+
const waitMs = Math.min(Math.pow(2, i) * 1000 + Math.floor(Math.random() * 200), 30000);
|
|
274
|
+
console.warn(`Rate limited (429). Waiting ${waitMs}ms before retry...`);
|
|
275
|
+
await delay(waitMs);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
if (i === retries - 1) {
|
|
@@ -270,7 +284,7 @@ async function requestWithRetry(requestFunction, retries = 5, endpoint = '', thr
|
|
|
270
284
|
}
|
|
271
285
|
throw error;
|
|
272
286
|
}
|
|
273
|
-
const backoffTime = (Math.pow(2, i)
|
|
287
|
+
const backoffTime = Math.min(Math.pow(2, i) * 1000 + Math.floor(Math.random() * 200), 30000);
|
|
274
288
|
console.warn(`Request attempt ${i + 1} failed. Retrying in ${backoffTime}ms...`);
|
|
275
289
|
await delay(backoffTime);
|
|
276
290
|
}
|
package/src/utils/clients.js
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
const { makeParsable, log, warn } = require("./constants");
|
|
4
4
|
const { globalRateLimiter, configureRateLimiter } = require("./rateLimiter");
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Safely emits an event on ctx._emitter without throwing if the emitter is absent.
|
|
8
|
+
* @param {Object} ctx - Application context.
|
|
9
|
+
* @param {string} event - Event name.
|
|
10
|
+
* @param {*} payload - Event payload.
|
|
11
|
+
*/
|
|
12
|
+
function _emit(ctx, event, payload) {
|
|
13
|
+
try {
|
|
14
|
+
if (ctx && ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
15
|
+
ctx._emitter.emit(event, payload);
|
|
16
|
+
}
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Attempts auto-login via ctx.performAutoLogin (wired by loginHelper).
|
|
22
|
+
* On success returns the original res so callers can continue transparently.
|
|
23
|
+
* On failure throws a SESSION_EXPIRED error with requiresReLogin = true.
|
|
24
|
+
* @param {Object} ctx
|
|
25
|
+
* @param {Object} http
|
|
26
|
+
* @param {*} res - Parsed JSON response body (passed through on success).
|
|
27
|
+
* @param {number} retryCount
|
|
28
|
+
* @returns {Promise<*>}
|
|
29
|
+
*/
|
|
30
|
+
async function _maybeAutoLogin(ctx, http, res, retryCount) {
|
|
31
|
+
if (ctx && !ctx.auto_login && typeof ctx.performAutoLogin === 'function') {
|
|
32
|
+
ctx.auto_login = true;
|
|
33
|
+
try {
|
|
34
|
+
const ok = await ctx.performAutoLogin();
|
|
35
|
+
ctx.auto_login = false;
|
|
36
|
+
if (ok) {
|
|
37
|
+
_emit(ctx, 'autoLoginSuccess', { res });
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
ctx.auto_login = false;
|
|
42
|
+
_emit(ctx, 'autoLoginFailed', { error: e, res });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_emit(ctx, 'sessionExpired', { reason: 'login_redirect' });
|
|
46
|
+
const err = new Error("Session expired - Redirected to login page");
|
|
47
|
+
err.error = 401;
|
|
48
|
+
err.errorCode = 401;
|
|
49
|
+
err.errorType = "LOGIN_REDIRECT";
|
|
50
|
+
err.requiresReLogin = true;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
|
|
6
54
|
/**
|
|
7
55
|
* Formats a cookie array into a string for use in a cookie jar.
|
|
8
56
|
* @param {Array<string>} arr - An array containing cookie parts.
|
|
@@ -23,33 +71,14 @@ function formatCookie(arr, url) {
|
|
|
23
71
|
function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
24
72
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
73
|
|
|
26
|
-
/**
|
|
27
|
-
* Attempt performAutoLogin if available and not already in progress.
|
|
28
|
-
* Returns true if re-login succeeded, false otherwise.
|
|
29
|
-
* Always resets ctx.auto_login in a finally block.
|
|
30
|
-
*/
|
|
31
|
-
async function tryAutoLogin() {
|
|
32
|
-
if (ctx.auto_login || typeof ctx.performAutoLogin !== 'function') return false;
|
|
33
|
-
ctx.auto_login = true;
|
|
34
|
-
try {
|
|
35
|
-
const ok = await ctx.performAutoLogin();
|
|
36
|
-
return !!ok;
|
|
37
|
-
} catch (_) {
|
|
38
|
-
return false;
|
|
39
|
-
} finally {
|
|
40
|
-
ctx.auto_login = false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
74
|
return async (data) => {
|
|
45
75
|
if (data.statusCode === 401) {
|
|
46
|
-
warn("Session Status", "Session expired. Triggering auto re-login...");
|
|
47
|
-
await tryAutoLogin();
|
|
48
76
|
const err = new Error("Session expired - Authentication required");
|
|
49
77
|
err.error = 401;
|
|
50
78
|
err.errorCode = 401;
|
|
51
79
|
err.errorType = "AUTHENTICATION_REQUIRED";
|
|
52
80
|
err.requiresReLogin = true;
|
|
81
|
+
warn("Session Status", "Session expired. Re-login required.");
|
|
53
82
|
throw err;
|
|
54
83
|
}
|
|
55
84
|
|
|
@@ -59,15 +88,20 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
59
88
|
err.statusCode = data.statusCode;
|
|
60
89
|
err.res = data.body;
|
|
61
90
|
err.error = "Request retry failed. Check the `res` and `statusCode` property on this error.";
|
|
91
|
+
log(`parseAndCheckLogin: Max retries (5) reached for status ${data.statusCode}`);
|
|
62
92
|
throw err;
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
retryCount++;
|
|
66
|
-
const
|
|
96
|
+
const baseDelay = retryCount === 1 ? 1500 : 1000 * Math.pow(2, retryCount - 1);
|
|
97
|
+
const jitter = Math.floor(Math.random() * 200);
|
|
98
|
+
const retryTime = Math.min(baseDelay + jitter, 10000);
|
|
67
99
|
const url = data.request.uri.protocol + "//" + data.request.uri.hostname + data.request.uri.pathname;
|
|
100
|
+
warn(`parseAndCheckLogin: HTTP ${data.statusCode} — retrying (attempt ${retryCount}/5) after ${retryTime}ms`);
|
|
68
101
|
|
|
69
102
|
await delay(retryTime);
|
|
70
103
|
|
|
104
|
+
// Guard against undefined Content-Type header before splitting
|
|
71
105
|
const contentType = (data.request.headers && data.request.headers["content-type"]) || "";
|
|
72
106
|
if (contentType.split(";")[0].trim() === "multipart/form-data") {
|
|
73
107
|
const newData = await http.postFormData(
|
|
@@ -79,6 +113,10 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
79
113
|
);
|
|
80
114
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
81
115
|
} else {
|
|
116
|
+
// defaultFuncs.post signature: (url, jar, form, ctxx, customHeader)
|
|
117
|
+
// The 4th arg must be ctx (not ctx.globalOptions) — passing globalOptions
|
|
118
|
+
// here caused the retry to be treated as a raw network call without
|
|
119
|
+
// session context, missing auth headers and session inspection.
|
|
82
120
|
const newData = await http.post(
|
|
83
121
|
url,
|
|
84
122
|
ctx.jar,
|
|
@@ -121,9 +159,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
121
159
|
if (res.jsmods && res.jsmods.require && Array.isArray(res.jsmods.require[0]) && res.jsmods.require[0][0] === "Cookie") {
|
|
122
160
|
res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace("_js_", "");
|
|
123
161
|
const requireCookie = res.jsmods.require[0][3];
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
try { ctx.jar.setCookieSync(formatCookie(requireCookie, "messenger"), "https://www.messenger.com"); } catch (_) {}
|
|
162
|
+
ctx.jar.setCookie(formatCookie(requireCookie, "facebook"), "https://www.facebook.com");
|
|
163
|
+
ctx.jar.setCookie(formatCookie(requireCookie, "messenger"), "https://www.messenger.com");
|
|
127
164
|
}
|
|
128
165
|
|
|
129
166
|
if (res.jsmods && Array.isArray(res.jsmods.require)) {
|
|
@@ -135,9 +172,6 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
135
172
|
for (let j = 0; j < ctx.fb_dtsg.length; j++) {
|
|
136
173
|
ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j);
|
|
137
174
|
}
|
|
138
|
-
// jazoest MUST stay in sync with fb_dtsg — stale jazoest causes form-submission
|
|
139
|
-
// failures that Facebook treats as tamper attempts and flags as bot activity.
|
|
140
|
-
ctx.jazoest = `2${Array.from(ctx.fb_dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`;
|
|
141
175
|
}
|
|
142
176
|
}
|
|
143
177
|
}
|
|
@@ -149,16 +183,12 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
149
183
|
};
|
|
150
184
|
|
|
151
185
|
if (res.error && SESSION_EXPIRY_CODES[res.error]) {
|
|
152
|
-
warn("Session Status", `${SESSION_EXPIRY_CODES[res.error]} (Code: ${res.error}) — triggering auto re-login`);
|
|
153
|
-
// Fire re-login so the session is refreshed even though this request fails.
|
|
154
|
-
// Awaiting ensures the new session is ready before the error propagates to
|
|
155
|
-
// callers (e.g. MQTT reconnect) that would immediately retry.
|
|
156
|
-
await tryAutoLogin();
|
|
157
186
|
const err = new Error(SESSION_EXPIRY_CODES[res.error]);
|
|
158
187
|
err.error = res.error;
|
|
159
188
|
err.errorCode = res.error;
|
|
160
189
|
err.errorType = "SESSION_EXPIRED";
|
|
161
190
|
err.requiresReLogin = true;
|
|
191
|
+
warn("Session Status", `${SESSION_EXPIRY_CODES[res.error]} (Code: ${res.error})`);
|
|
162
192
|
throw err;
|
|
163
193
|
}
|
|
164
194
|
|
|
@@ -177,67 +207,61 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
177
207
|
err.errorType = res.error === 1357004 ? "CHECKPOINT" : res.error === 1357031 ? "LOCKED" : "BLOCKED";
|
|
178
208
|
err.requiresReLogin = res.error === 1357004 || res.error === 1357031;
|
|
179
209
|
warn("Account Status", `${ACCOUNT_ERROR_CODES[res.error]} (Code: ${res.error})`);
|
|
180
|
-
// For checkpoint and locked states, trigger re-login so the session can recover
|
|
181
|
-
if (err.requiresReLogin) {
|
|
182
|
-
await tryAutoLogin();
|
|
183
|
-
}
|
|
184
210
|
throw err;
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
if (res.error === 1357001 || (res.errorSummary && res.errorSummary.includes("blocked"))) {
|
|
188
214
|
const err = new Error("Facebook blocked the login");
|
|
189
|
-
err.error = "
|
|
215
|
+
err.error = "login_blocked";
|
|
190
216
|
err.errorType = "BLOCKED";
|
|
217
|
+
err.res = res;
|
|
218
|
+
_emit(ctx, "loginBlocked", { res });
|
|
191
219
|
throw err;
|
|
192
220
|
}
|
|
193
221
|
|
|
194
222
|
const resStr = JSON.stringify(res);
|
|
195
223
|
|
|
224
|
+
// Scraping warning checkpoint — emit event and attempt auto-login so the
|
|
225
|
+
// session can recover instead of crashing. Throwing immediately here
|
|
226
|
+
// escalates the checkpoint into a permanent ban.
|
|
196
227
|
if (resStr.includes("XCheckpointFBScrapingWarningController") || resStr.includes("601051028565049")) {
|
|
197
|
-
warn("Bot Detection", "Facebook checkpoint detected
|
|
198
|
-
|
|
199
|
-
|
|
228
|
+
warn("Bot Detection", "Facebook scraping-warning checkpoint detected — attempting auto-login recovery");
|
|
229
|
+
_emit(ctx, "checkpoint", { type: "scraping_warning", res });
|
|
230
|
+
try {
|
|
231
|
+
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
|
|
200
232
|
configureRateLimiter({ maxConcurrentRequests: 2 });
|
|
201
233
|
} catch (_) {}
|
|
202
|
-
|
|
203
|
-
// account suspension. This path is reached when inspectResponseForSessionIssues
|
|
204
|
-
// was skipped (ctx._skipSessionInspect or null ctx). We still throw after
|
|
205
|
-
// cleanup: `res` at this point is checkpoint JSON, not a valid API response,
|
|
206
|
-
// so returning it would corrupt the caller. The bypass ensures the NEXT
|
|
207
|
-
// retry finds a clean session.
|
|
208
|
-
try {
|
|
209
|
-
const { bypassScrapingWarning } = require('./checkpointBypass');
|
|
210
|
-
await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID, null);
|
|
211
|
-
warn("Bot Detection", "Scraping warning dismissed — checkpoint cleared for next retry");
|
|
212
|
-
} catch (bypassErr) {
|
|
213
|
-
warn("Bot Detection", `Scraping warning bypass failed: ${bypassErr && bypassErr.message ? bypassErr.message : String(bypassErr)}`);
|
|
214
|
-
}
|
|
215
|
-
const err = new Error("Facebook detected automated behavior - Account may be flagged");
|
|
216
|
-
err.error = "Bot checkpoint detected";
|
|
217
|
-
err.errorCode = "CHECKPOINT_SCRAPING";
|
|
218
|
-
err.errorType = "BOT_DETECTION";
|
|
219
|
-
err.requiresReLogin = true;
|
|
220
|
-
throw err;
|
|
234
|
+
return await _maybeAutoLogin(ctx, http, res, retryCount);
|
|
221
235
|
}
|
|
222
236
|
|
|
223
237
|
if (resStr.includes("1501092823525282")) {
|
|
224
|
-
warn("Bot Detection", "Critical bot checkpoint 282 detected!
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
|
|
238
|
+
warn("Bot Detection", "Critical bot checkpoint 282 detected! Please check your Facebook account.");
|
|
239
|
+
try {
|
|
240
|
+
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
|
|
228
241
|
configureRateLimiter({ maxConcurrentRequests: 1 });
|
|
229
242
|
} catch (_) {}
|
|
230
|
-
|
|
231
|
-
|
|
243
|
+
_emit(ctx, "checkpoint", { type: "282", res });
|
|
244
|
+
_emit(ctx, "checkpoint_282", { res });
|
|
245
|
+
const err = new Error("Checkpoint 282 detected");
|
|
246
|
+
err.error = "checkpoint_282";
|
|
232
247
|
err.errorCode = "CHECKPOINT_282";
|
|
233
248
|
err.errorType = "BOT_DETECTION_CRITICAL";
|
|
234
249
|
err.requiresReLogin = true;
|
|
250
|
+
err.res = res;
|
|
235
251
|
throw err;
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
if (resStr.includes("828281030927956")) {
|
|
239
|
-
warn("Bot Detection", "Bot checkpoint 956 detected
|
|
240
|
-
|
|
255
|
+
warn("Bot Detection", "Bot checkpoint 956 detected — account may be restricted");
|
|
256
|
+
_emit(ctx, "checkpoint", { type: "956", res });
|
|
257
|
+
_emit(ctx, "checkpoint_956", { res });
|
|
258
|
+
const err = new Error("Checkpoint 956 detected");
|
|
259
|
+
err.error = "checkpoint_956";
|
|
260
|
+
err.errorCode = "CHECKPOINT_956";
|
|
261
|
+
err.errorType = "BOT_DETECTION";
|
|
262
|
+
err.requiresReLogin = true;
|
|
263
|
+
err.res = res;
|
|
264
|
+
throw err;
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
// Only treat a redirect to login.php as a session expiry if the server
|
|
@@ -245,29 +269,13 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
245
269
|
// anywhere in the body JSON, which it does on authenticated pages as a
|
|
246
270
|
// navigation/share link, causing false-positive session expiry errors.
|
|
247
271
|
if (String(res.redirect || "").includes("login.php")) {
|
|
248
|
-
warn("Session Status", "Redirected to login page
|
|
249
|
-
await
|
|
250
|
-
const err = new Error("Session expired - Redirected to login page");
|
|
251
|
-
err.error = 401;
|
|
252
|
-
err.errorCode = 401;
|
|
253
|
-
err.errorType = "LOGIN_REDIRECT";
|
|
254
|
-
err.requiresReLogin = true;
|
|
255
|
-
throw err;
|
|
272
|
+
warn("Session Status", "Redirected to login page — attempting auto-login recovery");
|
|
273
|
+
return await _maybeAutoLogin(ctx, http, res, retryCount);
|
|
256
274
|
}
|
|
257
275
|
|
|
258
|
-
if (typeof data.body === 'string' && (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
data.body.includes('/checkpoint/block/?next')
|
|
262
|
-
)) {
|
|
263
|
-
warn("Session Status", "Detected login page or checkpoint redirect — triggering auto re-login");
|
|
264
|
-
await tryAutoLogin();
|
|
265
|
-
const err = new Error("Session expired - Redirected to login page");
|
|
266
|
-
err.error = 401;
|
|
267
|
-
err.errorCode = 401;
|
|
268
|
-
err.errorType = "LOGIN_REDIRECT";
|
|
269
|
-
err.requiresReLogin = true;
|
|
270
|
-
throw err;
|
|
276
|
+
if (typeof data.body === 'string' && (data.body.includes('<title>Facebook - Log In or Sign Up</title>') || data.body.includes('name="login_form"'))) {
|
|
277
|
+
warn("Session Status", "Detected login page redirect — session expired");
|
|
278
|
+
return await _maybeAutoLogin(ctx, http, res, retryCount);
|
|
271
279
|
}
|
|
272
280
|
|
|
273
281
|
return res;
|
|
@@ -276,25 +284,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
276
284
|
|
|
277
285
|
/**
|
|
278
286
|
* Saves cookies from a response to the cookie jar.
|
|
279
|
-
* Uses setCookieSync to guarantee cookies are saved before the next request fires.
|
|
280
|
-
* Facebook continuously rotates session cookies (xs, c_user, fr, etc.) — any missed
|
|
281
|
-
* rotation leaves the jar stale and Facebook forces an immediate logout.
|
|
282
|
-
*
|
|
283
287
|
* @param {Object} jar - The cookie jar instance.
|
|
284
288
|
* @returns {function(res: Object): Object} A function that processes the response and returns it.
|
|
285
289
|
*/
|
|
286
290
|
function saveCookies(jar) {
|
|
287
291
|
return function (res) {
|
|
288
|
-
const cookies =
|
|
292
|
+
const cookies = res.headers["set-cookie"] || [];
|
|
289
293
|
cookies.forEach(function (c) {
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
+
// Always attempt to save every Set-Cookie header to both domains.
|
|
295
|
+
// The old guard `c.indexOf(".facebook.com") > -1` silently dropped
|
|
296
|
+
// cookies whose domain attribute was `facebook.com` (no dot),
|
|
297
|
+
// `www.facebook.com`, or absent entirely. Facebook rotates critical
|
|
298
|
+
// session cookies (xs, c_user, fr, etc.) continuously — missing a
|
|
299
|
+
// single update causes the jar to go stale and Facebook forces logout.
|
|
300
|
+
try { jar.setCookie(c, "https://www.facebook.com"); } catch (_) {}
|
|
301
|
+
|
|
302
|
+
// Mirror to messenger.com so MQTT / Messenger API calls stay authed.
|
|
294
303
|
const c2 = c
|
|
295
304
|
.replace(/domain=[^;]*/i, "domain=.messenger.com")
|
|
296
305
|
.replace(/secure;?\s*/i, "");
|
|
297
|
-
try { jar.
|
|
306
|
+
try { jar.setCookie(c2, "https://www.messenger.com"); } catch (_) {}
|
|
298
307
|
});
|
|
299
308
|
return res;
|
|
300
309
|
};
|
|
@@ -309,6 +318,7 @@ function saveCookies(jar) {
|
|
|
309
318
|
function getAccessFromBusiness(jar, Options) {
|
|
310
319
|
return async function (res) {
|
|
311
320
|
const html = res ? res.body : null;
|
|
321
|
+
// Use the same axios wrapper used everywhere else — "request" module does not exist
|
|
312
322
|
const { get } = require("./axios");
|
|
313
323
|
try {
|
|
314
324
|
const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options, { noRef: true });
|
|
@@ -321,24 +331,14 @@ function getAccessFromBusiness(jar, Options) {
|
|
|
321
331
|
}
|
|
322
332
|
|
|
323
333
|
/**
|
|
324
|
-
* Retrieves all cookies from the jar for both Facebook and Messenger domains
|
|
325
|
-
* deduplicated by key + domain + path so stale copies don't shadow fresh ones.
|
|
334
|
+
* Retrieves all cookies from the jar for both Facebook and Messenger domains.
|
|
326
335
|
* @param {Object} jar - The cookie jar instance.
|
|
327
336
|
* @returns {Array<Object>} An array of cookie objects.
|
|
328
337
|
*/
|
|
329
338
|
function getAppState(jar) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const all = [];
|
|
334
|
-
for (const cookie of [...fbCookies, ...messengerCookies]) {
|
|
335
|
-
const id = `${cookie.key}|${cookie.domain || ""}|${cookie.path || "/"}`;
|
|
336
|
-
if (!seen.has(id)) {
|
|
337
|
-
seen.add(id);
|
|
338
|
-
all.push(cookie);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return all;
|
|
339
|
+
return jar
|
|
340
|
+
.getCookiesSync("https://www.facebook.com")
|
|
341
|
+
.concat(jar.getCookiesSync("https://www.messenger.com"));
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
module.exports = {
|