@lazyneoaz/metachat 1.0.7 → 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 +10 -8
- 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 +32 -4
- package/src/utils/clients.js +83 -30
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",
|
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,12 +77,29 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (isScrapingWarning) {
|
|
80
|
-
const err = new Error('Facebook scraping warning checkpoint detected.');
|
|
81
|
-
err.error = 'checkpoint_scraping';
|
|
82
|
-
err.res = body;
|
|
83
80
|
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
84
81
|
ctx._emitter.emit('checkpoint', { type: 'scraping_warning', res: body });
|
|
85
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;
|
|
88
|
+
try {
|
|
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
|
+
}
|
|
97
|
+
} catch (_) {}
|
|
98
|
+
ctx.auto_login = false;
|
|
99
|
+
}
|
|
100
|
+
const err = new Error('Facebook scraping warning checkpoint detected.');
|
|
101
|
+
err.error = 'checkpoint_scraping';
|
|
102
|
+
err.res = body;
|
|
86
103
|
throw err;
|
|
87
104
|
}
|
|
88
105
|
|
|
@@ -247,6 +264,17 @@ async function requestWithRetry(requestFunction, retries = 5, endpoint = '', thr
|
|
|
247
264
|
if (error.response) {
|
|
248
265
|
const adapted = adaptResponse(error.response);
|
|
249
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
|
+
}
|
|
250
278
|
}
|
|
251
279
|
|
|
252
280
|
if (i === retries - 1) {
|
|
@@ -256,7 +284,7 @@ async function requestWithRetry(requestFunction, retries = 5, endpoint = '', thr
|
|
|
256
284
|
}
|
|
257
285
|
throw error;
|
|
258
286
|
}
|
|
259
|
-
const backoffTime = (Math.pow(2, i)
|
|
287
|
+
const backoffTime = Math.min(Math.pow(2, i) * 1000 + Math.floor(Math.random() * 200), 30000);
|
|
260
288
|
console.warn(`Request attempt ${i + 1} failed. Retrying in ${backoffTime}ms...`);
|
|
261
289
|
await delay(backoffTime);
|
|
262
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.
|
|
@@ -40,12 +88,16 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
40
88
|
err.statusCode = data.statusCode;
|
|
41
89
|
err.res = data.body;
|
|
42
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}`);
|
|
43
92
|
throw err;
|
|
44
93
|
}
|
|
45
94
|
|
|
46
95
|
retryCount++;
|
|
47
|
-
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);
|
|
48
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`);
|
|
49
101
|
|
|
50
102
|
await delay(retryTime);
|
|
51
103
|
|
|
@@ -160,45 +212,56 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
160
212
|
|
|
161
213
|
if (res.error === 1357001 || (res.errorSummary && res.errorSummary.includes("blocked"))) {
|
|
162
214
|
const err = new Error("Facebook blocked the login");
|
|
163
|
-
err.error = "
|
|
215
|
+
err.error = "login_blocked";
|
|
164
216
|
err.errorType = "BLOCKED";
|
|
217
|
+
err.res = res;
|
|
218
|
+
_emit(ctx, "loginBlocked", { res });
|
|
165
219
|
throw err;
|
|
166
220
|
}
|
|
167
221
|
|
|
168
222
|
const resStr = JSON.stringify(res);
|
|
169
|
-
|
|
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.
|
|
170
227
|
if (resStr.includes("XCheckpointFBScrapingWarningController") || resStr.includes("601051028565049")) {
|
|
171
|
-
warn("Bot Detection", "Facebook checkpoint detected
|
|
228
|
+
warn("Bot Detection", "Facebook scraping-warning checkpoint detected — attempting auto-login recovery");
|
|
229
|
+
_emit(ctx, "checkpoint", { type: "scraping_warning", res });
|
|
172
230
|
try {
|
|
173
231
|
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
|
|
174
232
|
configureRateLimiter({ maxConcurrentRequests: 2 });
|
|
175
233
|
} catch (_) {}
|
|
176
|
-
|
|
177
|
-
err.error = "Bot checkpoint detected";
|
|
178
|
-
err.errorCode = "CHECKPOINT_SCRAPING";
|
|
179
|
-
err.errorType = "BOT_DETECTION";
|
|
180
|
-
err.requiresReLogin = true;
|
|
181
|
-
throw err;
|
|
234
|
+
return await _maybeAutoLogin(ctx, http, res, retryCount);
|
|
182
235
|
}
|
|
183
236
|
|
|
184
237
|
if (resStr.includes("1501092823525282")) {
|
|
185
|
-
warn("Bot Detection", "Critical bot checkpoint 282 detected!
|
|
186
|
-
log("Please check your Facebook account in a browser and complete any security checks.");
|
|
238
|
+
warn("Bot Detection", "Critical bot checkpoint 282 detected! Please check your Facebook account.");
|
|
187
239
|
try {
|
|
188
240
|
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
|
|
189
241
|
configureRateLimiter({ maxConcurrentRequests: 1 });
|
|
190
242
|
} catch (_) {}
|
|
191
|
-
|
|
192
|
-
|
|
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";
|
|
193
247
|
err.errorCode = "CHECKPOINT_282";
|
|
194
248
|
err.errorType = "BOT_DETECTION_CRITICAL";
|
|
195
249
|
err.requiresReLogin = true;
|
|
250
|
+
err.res = res;
|
|
196
251
|
throw err;
|
|
197
252
|
}
|
|
198
253
|
|
|
199
254
|
if (resStr.includes("828281030927956")) {
|
|
200
|
-
warn("Bot Detection", "Bot checkpoint 956 detected
|
|
201
|
-
|
|
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;
|
|
202
265
|
}
|
|
203
266
|
|
|
204
267
|
// Only treat a redirect to login.php as a session expiry if the server
|
|
@@ -206,23 +269,13 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
206
269
|
// anywhere in the body JSON, which it does on authenticated pages as a
|
|
207
270
|
// navigation/share link, causing false-positive session expiry errors.
|
|
208
271
|
if (String(res.redirect || "").includes("login.php")) {
|
|
209
|
-
warn("Session Status", "Redirected to login page -
|
|
210
|
-
|
|
211
|
-
err.error = 401;
|
|
212
|
-
err.errorCode = 401;
|
|
213
|
-
err.errorType = "LOGIN_REDIRECT";
|
|
214
|
-
err.requiresReLogin = true;
|
|
215
|
-
throw err;
|
|
272
|
+
warn("Session Status", "Redirected to login page — attempting auto-login recovery");
|
|
273
|
+
return await _maybeAutoLogin(ctx, http, res, retryCount);
|
|
216
274
|
}
|
|
217
275
|
|
|
218
276
|
if (typeof data.body === 'string' && (data.body.includes('<title>Facebook - Log In or Sign Up</title>') || data.body.includes('name="login_form"'))) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
err.errorCode = 401;
|
|
222
|
-
err.errorType = "LOGIN_REDIRECT";
|
|
223
|
-
err.requiresReLogin = true;
|
|
224
|
-
warn("Session Status", "Detected login page redirect. Session expired.");
|
|
225
|
-
throw err;
|
|
277
|
+
warn("Session Status", "Detected login page redirect — session expired");
|
|
278
|
+
return await _maybeAutoLogin(ctx, http, res, retryCount);
|
|
226
279
|
}
|
|
227
280
|
|
|
228
281
|
return res;
|