@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "1.0.8",
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) => {
@@ -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);
@@ -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
- { theme_color: "FF000000", theme_id: "788274591712841", theme_emoji: "🖤", gradient: '["FFF0F0F0"]', should_show_icon: "", theme_name_with_subtitle: "Monochrome" },
8
- { theme_color: "FFFF5CA1", theme_id: "169463077092846", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Hot Pink" },
9
- { theme_color: "FF2825B5", theme_id: "271607034185782", theme_emoji: null, gradient: '["FF5E007E","FF331290","FF2825B5"]', should_show_icon: "1", theme_name_with_subtitle: "Shadow" },
10
- { theme_color: "FFD9A900", theme_id: "2533652183614000", theme_emoji: null, gradient: '["FF550029","FFAA3232","FFD9A900"]', should_show_icon: "1", theme_name_with_subtitle: "Maple" },
11
- { theme_color: "FFFB45DE", theme_id: "2873642949430623", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Tulip" },
12
- { theme_color: "FF5E007E", theme_id: "193497045377796", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Grape" },
13
- { theme_color: "FF7AA286", theme_id: "1455149831518874", theme_emoji: "🌑", gradient: '["FF25C0E1","FFCE832A"]', should_show_icon: "", theme_name_with_subtitle: "Dune" },
14
- { theme_color: "FFFAAF00", theme_id: "672058580051520", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Honey" },
15
- { theme_color: "FF0084FF", theme_id: "196241301102133", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Default Blue" },
16
- { theme_color: "FFFFC300", theme_id: "174636906462322", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Yellow" },
17
- { theme_color: "FF44BEC7", theme_id: "1928399724138152", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Teal Blue" },
18
- { theme_color: "FF7646FF", theme_id: "234137870477637", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Bright Purple" },
19
- { theme_color: "FFF25C54", theme_id: "3022526817824329", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Peach" },
20
- { theme_color: "FFF01D6A", theme_id: "724096885023603", theme_emoji: null, gradient: '["FF005FFF","FF9200FF","FFFF2E19"]', should_show_icon: "1", theme_name_with_subtitle: "Berry" },
21
- { theme_color: "FFFF7CA8", theme_id: "624266884847972", theme_emoji: null, gradient: '["FFFF8FB2","FFA797FF","FF00E5FF"]', should_show_icon: "1", theme_name_with_subtitle: "Candy" },
22
- { theme_color: "FF0084FF", theme_id: "196241301102133", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Classic" },
23
- { theme_color: "FF0099FF", theme_id: "3273938616164733", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Classic" },
24
- { theme_color: "FFFA3C4C", theme_id: "2129984390566328", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Red" },
25
- { theme_color: "FF13CF13", theme_id: "2136751179887052", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Green" },
26
- { theme_color: "FFFF7E29", theme_id: "175615189761153", theme_emoji: null, gradient: null, should_show_icon: "1", theme_name_with_subtitle: "Orange" }
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 themeColor = data.extensible_message_admin_text.theme_color;
67
- const colorMatch = THEME_COLORS.find(color => color.theme_color === themeColor);
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: colorMatch || {
126
+ logMessageData: {
74
127
  theme_color: themeColor,
75
- theme_id: null,
76
- theme_emoji: null,
77
- gradient: null,
78
- should_show_icon: null,
79
- theme_name_with_subtitle: null
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
  };
@@ -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: info.theme_id || info.themeID || null,
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
- is_default: !info.color && !info.theme_id
98
+ threadTheme: info.threadTheme || null,
99
+ is_default: !info.color && !themeId
96
100
  };
97
101
 
98
102
  if (callback) {
@@ -203,7 +203,7 @@ module.exports = function (defaultFuncs, api, ctx) {
203
203
  }
204
204
 
205
205
  const form = {
206
- av: ctx.globalOptions.pageID,
206
+ av: ctx.globalOptions.pageID || ctx.userID,
207
207
  queries: JSON.stringify({
208
208
  o0: {
209
209
  doc_id: "1498317363570230",
@@ -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
- messageThread.customization_info.outgoing_bubble_color
118
- ? messageThread.customization_info.outgoing_bubble_color.slice(2)
119
- : null,
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
- messageThread.customization_info.outgoing_bubble_color
108
- ? messageThread.customization_info.outgoing_bubble_color.slice(2)
109
- : null,
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
@@ -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
- const typ = {
453
- type: "typ",
454
- isTyping: !!jsonMessage.state,
455
- from: jsonMessage.sender_fbid.toString(),
456
- threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
457
- };
458
- globalCallback(null, typ);
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 (!ctx.globalOptions.updatePresence && jsonMessage.list) {
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
- const isNotLoggedIn = /Not logged in/i.test(msg);
813
- const isLoginBlocked = /blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir/i.test(msg);
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;
@@ -50,23 +50,20 @@ module.exports = function (defaultFuncs, api, ctx) {
50
50
 
51
51
  callback(null);
52
52
  } else {
53
- if (!ctx.mqttClient || !ctx.mqttClient.connected) {
54
- const err = new Error("markAsRead requires an active MQTT connection. Call api.listenMqtt() first.");
55
- callback(err);
56
- return returnPromise;
57
- }
53
+ const form = {
54
+ ["ids[" + threadID + "]"]: read,
55
+ watermarkTimestamp: Date.now(),
56
+ shouldSendReadReceipt: true,
57
+ };
58
58
 
59
- const publishErr = await new Promise((r) =>
60
- ctx.mqttClient.publish(
61
- "/mark_thread",
62
- JSON.stringify({ threadID, mark: "read", state: read }),
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 (publishErr) {
69
- callback(publishErr instanceof Error ? publishErr : new Error(String(publishErr)));
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.selfListen && fmtEvent2 && fmtEvent2.author && fmtEvent2.author.toString() === ctx.userID) return;
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",
@@ -99,7 +99,7 @@ module.exports = function (defaultFuncs, api, ctx) {
99
99
  };
100
100
 
101
101
  const context = {
102
- app_id: ctx.appID,
102
+ app_id: ctx.appID || "2220391788200892",
103
103
  payload: {
104
104
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
105
105
  tasks: [query],
@@ -34,11 +34,13 @@ function extractAndSearchLightspeedRequest(allJsonData, options = {}) {
34
34
  }
35
35
 
36
36
 
37
- try {
38
- fs.writeFileSync(outputFile, JSON.stringify(lightReq, null, 2), "utf8");
39
- utils.log(`pin.js: Saved lightspeed_web_request to ${outputFile}`);
40
- } catch (err) {
41
- utils.error("pin.js: Failed to write lightspeed_web_request.json", err);
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 (!reaction) throw new Error("Please enter a valid emoji.");
9
- const defData = await defaultFuncs.postFormData("https://www.facebook.com/webgraphql/mutation/", ctx.jar, {}, {
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
- data: {
13
- client_mutation_id: ctx.clientMutationId++,
14
- actor_id: ctx.userID,
15
- action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
16
- message_id: messageID,
17
- reaction
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: "370940413392601",
40
- green: "169463077092846",
41
- pink: "230032715012014",
42
- orange: "175615189761153",
43
- red: "2136751179887052",
44
- yellow: "2058653964378557",
45
- teal: "417639218648241",
46
- black: "539927563794799",
47
- white: "2873642392710980",
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
  };
@@ -77,22 +77,29 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
77
77
  }
78
78
 
79
79
  if (isScrapingWarning) {
80
- // Auto-dismiss before propagating the error. Unhandled scraping warnings
81
- // escalate to permanent suspension. We await the bypass so the checkpoint is
82
- // gone before the caller's retry (e.g. MQTT reconnect) fires.
83
- // Guards: only attempt when ctx has jar + globalOptions available.
84
- if (ctx && ctx.jar && ctx.globalOptions) {
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 { bypassScrapingWarning } = require('./checkpointBypass');
87
- await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID || null, null);
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)) * 1000 + Math.floor(Math.random() * 1000);
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
  }
@@ -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 retryTime = Math.floor(Math.random() * 5000);
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
- // Use setCookieSync to avoid async fire-and-forget that drops rotated session cookies
125
- try { ctx.jar.setCookieSync(formatCookie(requireCookie, "facebook"), "https://www.facebook.com"); } catch (_) {}
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 = "Not logged in.";
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 - scraping warning (601051028565049) — auto-dismissing");
198
- try {
199
- globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
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
- // Auto-dismiss the scraping warning — unhandled warnings escalate to permanent
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! Account requires immediate attention!");
225
- log("Please check your Facebook account in a browser and complete any security checks.");
226
- try {
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
- const err = new Error("Facebook detected automated behavior - Critical checkpoint required");
231
- err.error = "Critical bot checkpoint";
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 - Account may be restricted");
240
- log("Please verify your Facebook account status in a browser.");
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 - Session expired triggering auto re-login");
249
- await tryAutoLogin();
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
- data.body.includes('<title>Facebook - Log In or Sign Up</title>') ||
260
- data.body.includes('name="login_form"') ||
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 = (res.headers && res.headers["set-cookie"]) || [];
292
+ const cookies = res.headers["set-cookie"] || [];
289
293
  cookies.forEach(function (c) {
290
- // Save to facebook.com
291
- try { jar.setCookieSync(c, "https://www.facebook.com"); } catch (_) {}
292
-
293
- // Mirror to messenger.com so MQTT / Messenger API calls stay authenticated.
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.setCookieSync(c2, "https://www.messenger.com"); } catch (_) {}
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
- const fbCookies = jar.getCookiesSync("https://www.facebook.com");
331
- const messengerCookies = jar.getCookiesSync("https://www.messenger.com");
332
- const seen = new Set();
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 = {