@lazyneoaz/metachat 1.0.0 → 1.0.2
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 +5 -6
- package/src/apis/enableAutoSaveAppState.js +15 -4
- package/src/engine/models/buildAPI.js +1 -1
- package/src/engine/models/loginHelper.js +17 -3
- package/src/types/index.d.ts +1 -1
- package/src/utils/antiSuspension.js +13 -11
- package/src/utils/axios.js +4 -3
- package/src/utils/checkpointBypass.js +138 -0
- package/src/utils/tokenRefresh.js +34 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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.",
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"index.js",
|
|
10
10
|
"src/",
|
|
11
|
-
"LICENSE"
|
|
12
|
-
"README.md"
|
|
11
|
+
"LICENSE"
|
|
13
12
|
],
|
|
14
13
|
"repository": {
|
|
15
14
|
"type": "git",
|
|
@@ -41,18 +40,18 @@
|
|
|
41
40
|
"axios": "^1.13.5",
|
|
42
41
|
"axios-cookiejar-support": "^4.0.7",
|
|
43
42
|
"bluebird": "^3.7.2",
|
|
44
|
-
"cheerio": "^1.0.0",
|
|
43
|
+
"cheerio": "^1.0.0-rc.12",
|
|
45
44
|
"cli-progress": "^3.12.0",
|
|
46
45
|
"deepdash": "^5.3.9",
|
|
47
46
|
"duplexify": "^4.1.3",
|
|
48
47
|
"form-data": "^4.0.4",
|
|
49
|
-
"gradient-string": "^
|
|
48
|
+
"gradient-string": "^2.0.2",
|
|
50
49
|
"https-proxy-agent": "^7.0.6",
|
|
51
50
|
"jsonpath-plus": "^10.3.0",
|
|
52
51
|
"lodash": "^4.17.21",
|
|
53
52
|
"mqtt": "^4.3.8",
|
|
54
53
|
"node-cron": "^3.0.3",
|
|
55
|
-
"ora": "^
|
|
54
|
+
"ora": "^5.4.1",
|
|
56
55
|
"picocolors": "^1.1.1",
|
|
57
56
|
"sequelize": "^6.37.5",
|
|
58
57
|
"sqlite3": "^5.1.7",
|
|
@@ -22,10 +22,10 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
22
22
|
return function enableAutoSaveAppState(options) {
|
|
23
23
|
options = options || {};
|
|
24
24
|
const filePath = options.filePath || path.join(process.cwd(), "appstate.json");
|
|
25
|
-
const interval = options.interval ||
|
|
25
|
+
const interval = options.interval || 5 * 60 * 1000; // 5 min default (was 10 min)
|
|
26
26
|
const saveOnLogin = options.saveOnLogin !== false;
|
|
27
27
|
|
|
28
|
-
function saveAppState() {
|
|
28
|
+
async function saveAppState() {
|
|
29
29
|
try {
|
|
30
30
|
const appState = api.getAppState ? api.getAppState() : null;
|
|
31
31
|
if (!appState || (Array.isArray(appState) && appState.length === 0)) {
|
|
@@ -33,9 +33,20 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
33
33
|
}
|
|
34
34
|
const payload = Array.isArray(appState) ? appState : (appState.appState || appState);
|
|
35
35
|
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
36
|
-
} catch (
|
|
37
|
-
// Silently ignore write errors (e.g. read-only FS
|
|
36
|
+
} catch (_) {
|
|
37
|
+
// Silently ignore file-write errors (e.g. read-only FS)
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// Keep the database backup in sync so re-login always uses fresh cookies.
|
|
41
|
+
try {
|
|
42
|
+
const { backupAppStateSQL } = require('../database/appStateBackup');
|
|
43
|
+
const jar = (ctx && ctx.jar) ? ctx.jar : require('../utils').getJar();
|
|
44
|
+
const userID = (ctx && ctx.userID) ? ctx.userID :
|
|
45
|
+
(typeof api.getCurrentUserID === 'function' ? api.getCurrentUserID() : null);
|
|
46
|
+
if (jar && userID) {
|
|
47
|
+
await backupAppStateSQL(jar, userID);
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {}
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
let immediateTimer = null;
|
|
@@ -54,7 +54,7 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
54
54
|
|
|
55
55
|
const dtsgResult = {
|
|
56
56
|
fb_dtsg: dtsg,
|
|
57
|
-
jazoest: `2${Array.from(dtsg).reduce((a, b) => a + b.charCodeAt(0),
|
|
57
|
+
jazoest: `2${Array.from(dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`,
|
|
58
58
|
lsd: lsd
|
|
59
59
|
};
|
|
60
60
|
|
|
@@ -206,7 +206,19 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
|
|
|
206
206
|
globalAntiSuspension.enableWarmup();
|
|
207
207
|
} catch (_) {}
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
let resp = await utils.get(fbLinkFunc(), jar, null, globalOptions, { noRef: true }).then(utils.saveCookies(jar));
|
|
210
|
+
|
|
211
|
+
// Auto-dismiss the FBScrapingWarning checkpoint before building the API.
|
|
212
|
+
// If left unhandled this checkpoint escalates to a permanent account ban.
|
|
213
|
+
try {
|
|
214
|
+
const { bypassScrapingWarning } = require('../../utils/checkpointBypass');
|
|
215
|
+
const bypassedResp = await bypassScrapingWarning(jar, globalOptions, null, resp);
|
|
216
|
+
if (bypassedResp) {
|
|
217
|
+
resp = bypassedResp;
|
|
218
|
+
utils.log("LoginHelper", "Session restored after scraping-warning bypass");
|
|
219
|
+
}
|
|
220
|
+
} catch (_) {}
|
|
221
|
+
|
|
210
222
|
const extractNetData = (html) => {
|
|
211
223
|
const allScriptsData = [];
|
|
212
224
|
const scriptRegex = /<script type="application\/json"[^>]*>(.*?)<\/script>/g;
|
|
@@ -403,9 +415,11 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
|
|
|
403
415
|
// Use the lightweight presence endpoint instead of fetching the
|
|
404
416
|
// full homepage (~400 kB). Returns 200 JSON when authenticated,
|
|
405
417
|
// 302→login when the session is expired.
|
|
418
|
+
// Do NOT send fb_dtsg_ag= (empty) — real browsers always send a
|
|
419
|
+
// real token here, so an empty value is a bot fingerprint.
|
|
406
420
|
const resp = await utils.get(
|
|
407
|
-
'https://www.facebook.com/ajax/presence/reconnect.php
|
|
408
|
-
ctx.jar,
|
|
421
|
+
'https://www.facebook.com/ajax/presence/reconnect.php',
|
|
422
|
+
ctx.jar, { reason: '14', __a: '1' }, ctx.globalOptions, { noRef: true, _skipSessionInspect: true }
|
|
409
423
|
);
|
|
410
424
|
const html = resp.body || '';
|
|
411
425
|
|
package/src/types/index.d.ts
CHANGED
|
@@ -83,8 +83,8 @@ class AntiSuspension {
|
|
|
83
83
|
this.lastActivity = new Map();
|
|
84
84
|
this.typing = new Map();
|
|
85
85
|
|
|
86
|
-
this.messageDelayMs =
|
|
87
|
-
this.threadDelayMs =
|
|
86
|
+
this.messageDelayMs = 300;
|
|
87
|
+
this.threadDelayMs = 400;
|
|
88
88
|
this.loginAttempts = 0;
|
|
89
89
|
this.maxLoginAttempts = 3;
|
|
90
90
|
this.loginCooldown = 300000;
|
|
@@ -279,9 +279,11 @@ class AntiSuspension {
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
async addSmartDelay() {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
282
|
+
// Minimum realistic inter-request pause — short enough not to block
|
|
283
|
+
// high-throughput flows but long enough to avoid sub-100ms bot patterns.
|
|
284
|
+
const base = 500 + Math.random() * 700;
|
|
285
|
+
const jitter = (Math.random() - 0.5) * 150;
|
|
286
|
+
const total = Math.max(350, base + jitter);
|
|
285
287
|
await new Promise(resolve => setTimeout(resolve, total));
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -293,14 +295,14 @@ class AntiSuspension {
|
|
|
293
295
|
const threadCount = this.dailyStats.threadStats.get(String(threadID))?.count || 0;
|
|
294
296
|
const globalCount = this.dailyStats.messageCount;
|
|
295
297
|
|
|
296
|
-
let base =
|
|
297
|
-
if (globalCount > 800) base =
|
|
298
|
-
else if (globalCount > 400) base =
|
|
298
|
+
let base = 120;
|
|
299
|
+
if (globalCount > 800) base = 500;
|
|
300
|
+
else if (globalCount > 400) base = 250;
|
|
299
301
|
|
|
300
|
-
if (threadCount > 50) base +=
|
|
302
|
+
if (threadCount > 50) base += 180;
|
|
301
303
|
|
|
302
304
|
const jitter = Math.random() * base * 0.4;
|
|
303
|
-
const total = Math.max(
|
|
305
|
+
const total = Math.max(80, base + jitter);
|
|
304
306
|
await new Promise(resolve => setTimeout(resolve, total));
|
|
305
307
|
}
|
|
306
308
|
|
|
@@ -320,7 +322,7 @@ class AntiSuspension {
|
|
|
320
322
|
|
|
321
323
|
async enforceMessageRate() {
|
|
322
324
|
await new Promise(resolve =>
|
|
323
|
-
setTimeout(resolve, this.messageDelayMs + Math.random() *
|
|
325
|
+
setTimeout(resolve, this.messageDelayMs + Math.random() * 300)
|
|
324
326
|
);
|
|
325
327
|
}
|
|
326
328
|
|
package/src/utils/axios.js
CHANGED
|
@@ -111,18 +111,19 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
111
111
|
|
|
112
112
|
if (!ctx.auto_login && typeof ctx.performAutoLogin === 'function') {
|
|
113
113
|
ctx.auto_login = true;
|
|
114
|
+
// Always reset the flag in a finally block so a synchronous throw
|
|
115
|
+
// or a rejected promise can never leave auto_login stuck at true,
|
|
116
|
+
// which would permanently prevent future re-login attempts.
|
|
114
117
|
try {
|
|
115
118
|
const ok = await ctx.performAutoLogin();
|
|
116
|
-
ctx.auto_login = false;
|
|
117
119
|
if (!ok) {
|
|
118
120
|
const err = new Error('Not logged in. Auto re-login failed.');
|
|
119
121
|
err.error = 'Not logged in.';
|
|
120
122
|
err.res = body;
|
|
121
123
|
throw err;
|
|
122
124
|
}
|
|
123
|
-
}
|
|
125
|
+
} finally {
|
|
124
126
|
ctx.auto_login = false;
|
|
125
|
-
throw autoErr;
|
|
126
127
|
}
|
|
127
128
|
} else {
|
|
128
129
|
const err = new Error('Not logged in. Session has expired.');
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('./index');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects and auto-dismisses Facebook's FBScrapingWarning checkpoint.
|
|
7
|
+
*
|
|
8
|
+
* This checkpoint (at /checkpoint/601051028565049) is triggered when Facebook
|
|
9
|
+
* suspects automated access. If left unhandled it escalates to a permanent
|
|
10
|
+
* account suspension. The fix is to POST the FBScrapingWarningMutation GraphQL
|
|
11
|
+
* call with doc_id 6339492849481770, which silently acknowledges the warning.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} jar - The cookie jar.
|
|
14
|
+
* @param {object} globalOptions - Global request options.
|
|
15
|
+
* @param {string|null} userID - User ID (extracted from cookies/HTML if omitted).
|
|
16
|
+
* @param {object|null} existingResp - An already-fetched homepage response to inspect
|
|
17
|
+
* first. When provided, a fresh fetch is skipped
|
|
18
|
+
* unless the checkpoint is actually detected.
|
|
19
|
+
* @returns {Promise<object|null>} The refreshed response after bypass, or null if no
|
|
20
|
+
* checkpoint was present.
|
|
21
|
+
*/
|
|
22
|
+
async function bypassScrapingWarning(jar, globalOptions, userID = null, existingResp = null) {
|
|
23
|
+
try {
|
|
24
|
+
const toStr = (x) => (typeof x === 'string' ? x : JSON.stringify(x || ""));
|
|
25
|
+
|
|
26
|
+
const isCp = (resp) => {
|
|
27
|
+
if (!resp) return false;
|
|
28
|
+
const body = toStr(resp.body || resp.data || "");
|
|
29
|
+
return (
|
|
30
|
+
body.includes('checkpoint/601051028565049') ||
|
|
31
|
+
body.includes('XCheckpointFBScrapingWarningController')
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Use the existing response if provided, otherwise fetch the homepage.
|
|
36
|
+
let initial = existingResp || null;
|
|
37
|
+
if (!initial) {
|
|
38
|
+
initial = await utils.get(
|
|
39
|
+
'https://www.facebook.com/',
|
|
40
|
+
jar, null, globalOptions,
|
|
41
|
+
{ noRef: true, _skipSessionInspect: true }
|
|
42
|
+
);
|
|
43
|
+
utils.saveCookies(jar)(initial);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isCp(initial)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
utils.warn("BYPASS", "FBScrapingWarning checkpoint detected — auto-dismissing...");
|
|
51
|
+
|
|
52
|
+
const html = toStr(initial.body || initial.data || "");
|
|
53
|
+
|
|
54
|
+
// --- Extract user ID ---
|
|
55
|
+
let uid = userID || null;
|
|
56
|
+
if (!uid) {
|
|
57
|
+
try {
|
|
58
|
+
const cookies = jar.getCookiesSync('https://www.facebook.com');
|
|
59
|
+
const pick = (key) => (cookies.find(c => c.key === key) || {}).value;
|
|
60
|
+
uid = pick('i_user') || pick('c_user') || null;
|
|
61
|
+
} catch (_) {}
|
|
62
|
+
}
|
|
63
|
+
if (!uid) {
|
|
64
|
+
const m = html.match(/"USER_ID"\s*:\s*"(\d+)"/) ||
|
|
65
|
+
html.match(/\["CurrentUserInitialData",\[\],\{[^}]*"USER_ID":"(\d+)"/);
|
|
66
|
+
if (m) uid = m[1];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Extract CSRF tokens ---
|
|
70
|
+
const grab = (patterns) => {
|
|
71
|
+
for (const re of patterns) {
|
|
72
|
+
const m = html.match(re);
|
|
73
|
+
if (m && m[1]) return m[1];
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const fb_dtsg = grab([
|
|
79
|
+
/"DTSGInitData",\[\],\{"token":"([^"]+)"/,
|
|
80
|
+
/name="fb_dtsg"\s+value="([^"]+)"/,
|
|
81
|
+
/"DTSGInitialData",\[\],\{"token":"([^"]+)"/
|
|
82
|
+
]);
|
|
83
|
+
const jazoest = grab([
|
|
84
|
+
/name="jazoest"\s+value="([^"]+)"/,
|
|
85
|
+
/jazoest=(\d+)/
|
|
86
|
+
]);
|
|
87
|
+
const lsd = grab([
|
|
88
|
+
/"LSD",\[\],\{"token":"([^"]+)"/,
|
|
89
|
+
/name="lsd"\s+value="([^"]+)"/
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
if (!fb_dtsg || !uid) {
|
|
93
|
+
utils.warn("BYPASS", `Cannot bypass — missing tokens (uid=${uid ? 'ok' : 'missing'}, fb_dtsg=${fb_dtsg ? 'ok' : 'missing'})`);
|
|
94
|
+
return initial;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- POST FBScrapingWarningMutation ---
|
|
98
|
+
const form = {
|
|
99
|
+
av: uid,
|
|
100
|
+
fb_dtsg,
|
|
101
|
+
jazoest: jazoest || "",
|
|
102
|
+
lsd: lsd || "",
|
|
103
|
+
fb_api_caller_class: "RelayModern",
|
|
104
|
+
fb_api_req_friendly_name: "FBScrapingWarningMutation",
|
|
105
|
+
variables: "{}",
|
|
106
|
+
server_timestamps: "true",
|
|
107
|
+
doc_id: "6339492849481770"
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const mutResp = await utils.post(
|
|
111
|
+
'https://www.facebook.com/api/graphql/',
|
|
112
|
+
jar, form, globalOptions,
|
|
113
|
+
{ noRef: true, _skipSessionInspect: true }
|
|
114
|
+
);
|
|
115
|
+
utils.saveCookies(jar)(mutResp);
|
|
116
|
+
|
|
117
|
+
// --- Re-fetch homepage to confirm checkpoint is gone ---
|
|
118
|
+
const refreshed = await utils.get(
|
|
119
|
+
'https://www.facebook.com/',
|
|
120
|
+
jar, null, globalOptions,
|
|
121
|
+
{ noRef: true, _skipSessionInspect: true }
|
|
122
|
+
);
|
|
123
|
+
utils.saveCookies(jar)(refreshed);
|
|
124
|
+
|
|
125
|
+
if (isCp(refreshed)) {
|
|
126
|
+
utils.warn("BYPASS", "Checkpoint still present after FBScrapingWarningMutation — may need manual intervention");
|
|
127
|
+
} else {
|
|
128
|
+
utils.log("BYPASS", "FBScrapingWarning checkpoint dismissed. Session restored.");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return refreshed;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
utils.error("BYPASS", `bypassScrapingWarning error: ${err && err.message ? err.message : String(err)}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { bypassScrapingWarning };
|
|
@@ -95,6 +95,11 @@ class TokenRefreshManager {
|
|
|
95
95
|
probeCtx
|
|
96
96
|
);
|
|
97
97
|
|
|
98
|
+
// Save any rotated cookies Facebook returns (fr, xs, etc.) so the
|
|
99
|
+
// jar stays fresh. Dropping rotated cookies causes Facebook to
|
|
100
|
+
// treat the session as stale and forces a logout.
|
|
101
|
+
try { utils.saveCookies(ctx.jar)(resp); } catch (_) {}
|
|
102
|
+
|
|
98
103
|
const body = resp.body;
|
|
99
104
|
if (!body) return false;
|
|
100
105
|
|
|
@@ -140,13 +145,29 @@ class TokenRefreshManager {
|
|
|
140
145
|
const RETRY_DELAYS = [2000, 5000, 10000];
|
|
141
146
|
|
|
142
147
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
let resp = await utils.get(fbLink, ctx.jar, null, ctx.globalOptions, { noRef: true });
|
|
149
|
+
utils.saveCookies(ctx.jar)(resp);
|
|
150
|
+
|
|
151
|
+
let html = resp.body;
|
|
146
152
|
if (!html) {
|
|
147
153
|
throw new Error("Empty response from Facebook");
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
// Auto-dismiss FBScrapingWarning before any other checks
|
|
157
|
+
const isScrapingWarning = html.includes('XCheckpointFBScrapingWarningController') ||
|
|
158
|
+
html.includes('checkpoint/601051028565049');
|
|
159
|
+
if (isScrapingWarning) {
|
|
160
|
+
try {
|
|
161
|
+
const { bypassScrapingWarning } = require('./checkpointBypass');
|
|
162
|
+
const bypassed = await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID, resp);
|
|
163
|
+
if (bypassed) {
|
|
164
|
+
resp = bypassed;
|
|
165
|
+
html = resp.body || html;
|
|
166
|
+
utils.log("TokenRefresh", "Scraping-warning checkpoint dismissed during token refresh");
|
|
167
|
+
}
|
|
168
|
+
} catch (_) {}
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
// Precise check - broad html.includes("login") is a false positive because
|
|
151
172
|
// Facebook includes the word "login" all over authenticated pages too.
|
|
152
173
|
const isLoginPage = html.includes('<form id="login_form"') ||
|
|
@@ -172,6 +193,9 @@ class TokenRefreshManager {
|
|
|
172
193
|
for (let i = 0; i < ctx.fb_dtsg.length; i++) {
|
|
173
194
|
ctx.ttstamp += ctx.fb_dtsg.charCodeAt(i);
|
|
174
195
|
}
|
|
196
|
+
// Recalculate jazoest whenever fb_dtsg changes — it must stay in sync.
|
|
197
|
+
// jazoest = "2" + sum-of-char-codes (numeric sum, not concatenation).
|
|
198
|
+
ctx.jazoest = `2${Array.from(ctx.fb_dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`;
|
|
175
199
|
} else {
|
|
176
200
|
throw new Error("Failed to extract fb_dtsg token");
|
|
177
201
|
}
|
|
@@ -193,12 +217,19 @@ class TokenRefreshManager {
|
|
|
193
217
|
|
|
194
218
|
this.lastRefresh = Date.now();
|
|
195
219
|
this.failureCount = 0;
|
|
220
|
+
|
|
221
|
+
// Persist fresh cookies and update the DB backup so re-login
|
|
222
|
+
// attempts always use the newest session state.
|
|
196
223
|
try {
|
|
197
224
|
if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
198
225
|
const appState = utils.getAppState(ctx.jar);
|
|
199
226
|
globalAutoReLoginManager.updateAppState(appState);
|
|
200
227
|
}
|
|
201
228
|
} catch (_) {}
|
|
229
|
+
try {
|
|
230
|
+
const { backupAppStateSQL } = require('../database/appStateBackup');
|
|
231
|
+
await backupAppStateSQL(ctx.jar, ctx.userID);
|
|
232
|
+
} catch (_) {}
|
|
202
233
|
return true;
|
|
203
234
|
} catch (error) {
|
|
204
235
|
this.failureCount++;
|