scripter-x 1.0.23 → 1.0.25
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/commands.js +18 -9
- package/src/flipkart.js +50 -8
- package/src/util.js +33 -32
- package/src/worker.js +19 -0
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// stdin/stdout). Each command: async (io, api, args) => void.
|
|
4
4
|
import * as config from './config.js';
|
|
5
5
|
import { ApiClient, AuthError } from './api.js';
|
|
6
|
-
import { saveSessions, CANCEL } from './util.js';
|
|
6
|
+
import { saveSessions, saveSessionsLocal, CANCEL } from './util.js';
|
|
7
7
|
import * as tp from './providers/tempotp.js';
|
|
8
8
|
import * as oc from './providers/otpcart.js';
|
|
9
9
|
import * as kuku from './providers/kuku.js';
|
|
@@ -205,18 +205,27 @@ export async function run(io, api, args = {}) {
|
|
|
205
205
|
|
|
206
206
|
io.print(` ✓ done — ${stats.succeeded} succeeded · ${stats.failed} failed · ${stats.cancelled} cancelled · ₹${stats.charges} spent`);
|
|
207
207
|
if (stats.succeeded > 0) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
// PRIMARY: save LOCALLY from the worker's in-memory buffer — never depends on the
|
|
209
|
+
// server, so an expired dashboard JWT can't lose the JSONs the user paid for.
|
|
210
|
+
let saved = null;
|
|
211
|
+
try {
|
|
212
|
+
saved = saveSessionsLocal(worker.results, cid, { campaignName: name, out: args.out, checkMinutes });
|
|
213
|
+
} catch (e) {
|
|
214
|
+
io.print(` ! local save error: ${e.message}`, 'danger');
|
|
215
|
+
}
|
|
216
|
+
// FALLBACK: only if the local buffer was somehow empty, try the server export.
|
|
217
|
+
if (!saved && worker.results.length === 0) {
|
|
218
|
+
for (let a = 0; a < 3 && !saved; a++) {
|
|
219
|
+
if (a > 0) await new Promise((r) => setTimeout(r, 1000));
|
|
220
|
+
try { saved = await saveSessions(api, cid, { campaignName: name, out: args.out }); } catch { /* */ }
|
|
221
|
+
}
|
|
212
222
|
}
|
|
213
223
|
if (saved) {
|
|
214
224
|
io.print(' ✓ saved session(s) to:');
|
|
215
|
-
for (const res of saved) {
|
|
216
|
-
|
|
217
|
-
|
|
225
|
+
for (const res of saved) io.print(` ${res.label} ${res.path} (${res.count} sessions)`, 'accent');
|
|
226
|
+
} else {
|
|
227
|
+
io.print(' ! could not write the session file — check disk permissions', 'danger');
|
|
218
228
|
}
|
|
219
|
-
else io.print(` ! couldn't auto-save${lastErr ? ` (${lastErr.message})` : ''} — run \`export ${name}\` to retry`);
|
|
220
229
|
} else io.print(' ◉ no sessions extracted — nothing to save.');
|
|
221
230
|
if (worker.logPath) io.print(` ◉ log saved to: ${worker.logPath}`);
|
|
222
231
|
}
|
package/src/flipkart.js
CHANGED
|
@@ -85,6 +85,7 @@ export class FlipkartLogin {
|
|
|
85
85
|
this.reqId = null;
|
|
86
86
|
this.sn = '';
|
|
87
87
|
this.vid = '';
|
|
88
|
+
this.isNewUser = false; // set by sendOtp: new number → verify as SIGNUP, not LOGIN
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async sendOtp(mobile) {
|
|
@@ -117,37 +118,78 @@ export class FlipkartLogin {
|
|
|
117
118
|
// no requestId on a 200 = soft anti-bot throttle — also retryable (often clears on retry).
|
|
118
119
|
if (!rid) throw new TransientError('blocked while sending OTP (no requestId — throttled)');
|
|
119
120
|
this.reqId = rid;
|
|
121
|
+
// NEW vs EXISTING account: for an UNREGISTERED number FK still sends the OTP but
|
|
122
|
+
// routes the flow to signup. The reliable signals (verified against captures):
|
|
123
|
+
// • landingPageAction.url starts with "/signup" (existing accounts: no such url)
|
|
124
|
+
// • eVar79 tracking is "New|…|Sign-Up|…" (existing: "Existing|…|Sign-In|…")
|
|
125
|
+
// NOTE: a bare "isNewUser":true appears even for EXISTING accounts (unrelated page
|
|
126
|
+
// flag) — do NOT use it. verifyOtp uses type:SIGNUP when isNewUser, else LOGIN.
|
|
127
|
+
const landingUrl = data?.RESPONSE?.actionResponseContext?.landingPageAction?.url || '';
|
|
128
|
+
const eVar79 = data?.RESPONSE?.pageResponse?.pageData?.trackingContext?.tracking?.eVar79 || '';
|
|
129
|
+
this.isNewUser = /^\/signup\b/.test(landingUrl)
|
|
130
|
+
|| /\bSign-?Up\b/i.test(eVar79)
|
|
131
|
+
|| /^New\b/i.test(eVar79);
|
|
120
132
|
this.sn = this.jar.SN || '';
|
|
121
133
|
this.vid = this.sn ? this.sn.split('.')[0] : '';
|
|
122
134
|
return rid;
|
|
123
135
|
}
|
|
124
136
|
|
|
125
|
-
|
|
137
|
+
// Low-level OTP verify with an explicit action type ("LOGIN" for existing
|
|
138
|
+
// accounts, "SIGNUP" for brand-new numbers). Returns { res, text, data }.
|
|
139
|
+
async _verifyWith(type, mobile, otp) {
|
|
126
140
|
await throttle.wait();
|
|
127
141
|
const secureToken = this.vid ? `${this.vid}:${this.vid}` : '';
|
|
128
142
|
const body = { actionRequestContext: {
|
|
129
|
-
type
|
|
143
|
+
type, loginIdPrefix: '+91', loginId: mobile.slice(-10), password: null,
|
|
130
144
|
otp, otpRequestId: this.reqId, remainingAttempts: 5, phoneNumberFormat: 'E164',
|
|
131
|
-
loginType: 'MOBILE', verificationType: 'OTP', sourceContext: '
|
|
132
|
-
otpRegex: null, data: null, clientQueryParamMap: null } };
|
|
145
|
+
loginType: 'MOBILE', verificationType: 'OTP', sourceContext: type === 'SIGNUP' ? 'DEFAULT' : 'GO_LOGIN',
|
|
146
|
+
churned: false, otpRegex: null, data: null, clientQueryParamMap: null } };
|
|
133
147
|
const res = await fetch(`${NATIVE_HOST}/1/action/view`, {
|
|
134
148
|
method: 'POST', body: JSON.stringify(body),
|
|
135
149
|
headers: {
|
|
136
150
|
'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(this.vid),
|
|
137
151
|
'Content-Type': 'application/json; charset=UTF-8',
|
|
138
|
-
'x-request-metaInfo':
|
|
152
|
+
'x-request-metaInfo': `{"actionType":"${type}","pageUri":"questContext"}`,
|
|
139
153
|
at: this.jar.at || '', sn: this.sn, secureToken, secureCookie: this.jar.S || '',
|
|
140
154
|
Cookie: cookieHeader(this.jar),
|
|
141
155
|
},
|
|
142
156
|
});
|
|
143
157
|
const text = await res.text();
|
|
158
|
+
let data = null;
|
|
159
|
+
try { data = JSON.parse(text); } catch { /* caller handles */ }
|
|
160
|
+
return { res, text, data };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async verifyOtp(mobile, otp) {
|
|
164
|
+
// New number → SIGNUP (FK rejects LOGIN with "account does not exist"); existing → LOGIN.
|
|
165
|
+
const primary = this.isNewUser ? 'SIGNUP' : 'LOGIN';
|
|
166
|
+
const fallback = this.isNewUser ? 'LOGIN' : 'SIGNUP';
|
|
167
|
+
|
|
168
|
+
let { res, text, data } = await this._verifyWith(primary, mobile, otp);
|
|
144
169
|
// 529/5xx = Flipkart overloaded / soft-blocking this IP — NOT the OTP's fault.
|
|
145
170
|
if (res.status === 429 || res.status === 529 || res.status >= 500) {
|
|
146
171
|
throw new TransientError(`Flipkart busy (HTTP ${res.status}) — will retry`);
|
|
147
172
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
173
|
+
if (!data) throw new TransientError('Flipkart returned a non-JSON response — will retry');
|
|
174
|
+
|
|
175
|
+
let env = data.SESSION;
|
|
176
|
+
// If the primary type didn't log in AND the failure looks like a wrong account-type
|
|
177
|
+
// (not a wrong OTP), retry once with the other type. This auto-creates a new account
|
|
178
|
+
// when a number we thought existed doesn't (and vice-versa) — same OTP, no extra SMS.
|
|
179
|
+
if ((!env || !env.isLoggedIn)) {
|
|
180
|
+
const fkMsg0 = flipkartErrorMessage(data, text);
|
|
181
|
+
const low0 = (fkMsg0 + ' ' + text).toLowerCase();
|
|
182
|
+
const wrongType = /does not exist|not registered|no account|sign\s*up|signup|already (registered|exists)|please login/.test(low0);
|
|
183
|
+
const wrongOtp = /incorrect|wrong|invalid|expired|otp.*not.*match|verification code/.test(low0);
|
|
184
|
+
if (wrongType && !wrongOtp) {
|
|
185
|
+
const r2 = await this._verifyWith(fallback, mobile, otp);
|
|
186
|
+
if (r2.data && r2.data.SESSION && r2.data.SESSION.isLoggedIn) {
|
|
187
|
+
({ res, text, data } = r2);
|
|
188
|
+
env = data.SESSION;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
151
193
|
if (!env || !env.isLoggedIn) {
|
|
152
194
|
// pull Flipkart's own human message if present (the real reason)
|
|
153
195
|
const fkMsg = flipkartErrorMessage(data, text);
|
package/src/util.js
CHANGED
|
@@ -31,34 +31,19 @@ export const log = {
|
|
|
31
31
|
err: (m) => console.log(` ${paint.err('✗')} ${m}`),
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
let name = (campaignName || cid.slice(0, 8)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || cid.slice(0, 8);
|
|
39
|
-
const accounts = await api.exportCampaign(cid);
|
|
34
|
+
// Core: partition an accounts array by Minutes status and write the JSON file(s).
|
|
35
|
+
// accounts = [{ session, coupon_eligible? }]. Pure local I/O — no server.
|
|
36
|
+
// Returns [{label, path, count}] or null.
|
|
37
|
+
function writeAccounts(accounts, { name, out, checkMinutes } = {}) {
|
|
40
38
|
if (!accounts || !accounts.length) return null;
|
|
41
39
|
|
|
42
|
-
let checkMinutes = false;
|
|
43
|
-
try {
|
|
44
|
-
const { campaign } = await api.getCampaign(cid);
|
|
45
|
-
checkMinutes = !!campaign.check_minutes;
|
|
46
|
-
} catch (e) {
|
|
47
|
-
// ignore
|
|
48
|
-
}
|
|
49
|
-
|
|
50
40
|
// Resolve base directory and optional forced filename.
|
|
51
41
|
let baseDir, forcedName = null;
|
|
52
|
-
const defaultName = `${name}-${cid.slice(0, 8)}.json`;
|
|
53
42
|
if (out) {
|
|
54
43
|
const expanded = out.startsWith('~') ? join(homedir(), out.slice(1)) : out;
|
|
55
44
|
const isDir = (existsSync(expanded) && statSync(expanded).isDirectory()) || /[/\\]$/.test(out);
|
|
56
|
-
if (isDir)
|
|
57
|
-
|
|
58
|
-
} else {
|
|
59
|
-
baseDir = dirname(expanded);
|
|
60
|
-
forcedName = expanded.split(/[/\\]/).pop();
|
|
61
|
-
}
|
|
45
|
+
if (isDir) baseDir = expanded;
|
|
46
|
+
else { baseDir = dirname(expanded); forcedName = expanded.split(/[/\\]/).pop(); }
|
|
62
47
|
} else {
|
|
63
48
|
const downloads = join(homedir(), 'Downloads');
|
|
64
49
|
const base = existsSync(downloads) ? downloads : homedir();
|
|
@@ -67,22 +52,19 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
|
|
|
67
52
|
|
|
68
53
|
function write(suffix, sessionsToWrite) {
|
|
69
54
|
if (!sessionsToWrite || !sessionsToWrite.length) return null;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} else {
|
|
74
|
-
fname = `${name}-${cid.slice(0, 8)}${suffix}.json`;
|
|
75
|
-
}
|
|
55
|
+
const fname = forcedName
|
|
56
|
+
? (suffix === '' ? forcedName : forcedName.replace(/\.json$/i, `${suffix}.json`))
|
|
57
|
+
: `${name}${suffix}.json`;
|
|
76
58
|
const dest = join(baseDir, fname);
|
|
77
59
|
mkdirSync(baseDir, { recursive: true });
|
|
78
60
|
writeFileSync(dest, JSON.stringify(sessionsToWrite, null, 2));
|
|
79
61
|
return dest;
|
|
80
62
|
}
|
|
81
63
|
|
|
82
|
-
// Partition by coupon_eligible when
|
|
64
|
+
// Partition by coupon_eligible when the Minutes check was requested.
|
|
83
65
|
const minutesFree = []; // coupon_eligible=true → no Minutes order, ₹100 coupon available
|
|
84
66
|
const minutesUsed = []; // coupon_eligible=false → already placed a Minutes order
|
|
85
|
-
const unchecked = []; // coupon_eligible absent → check wasn't done
|
|
67
|
+
const unchecked = []; // coupon_eligible absent → check wasn't done
|
|
86
68
|
|
|
87
69
|
let hasChecked = false;
|
|
88
70
|
for (const a of accounts) {
|
|
@@ -101,10 +83,8 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
|
|
|
101
83
|
if (hasChecked || (checkMinutes && (minutesFree.length || minutesUsed.length))) {
|
|
102
84
|
const destFree = write('-minutes-free', minutesFree);
|
|
103
85
|
if (destFree) results.push({ label: '🟢 minutes-free (₹100 coupon)', path: destFree, count: minutesFree.length });
|
|
104
|
-
|
|
105
86
|
const destUsed = write('-minutes-used', minutesUsed);
|
|
106
87
|
if (destUsed) results.push({ label: '🔴 minutes-used (coupon gone)', path: destUsed, count: minutesUsed.length });
|
|
107
|
-
|
|
108
88
|
if (unchecked.length) {
|
|
109
89
|
const destUnk = write('-unchecked', unchecked);
|
|
110
90
|
if (destUnk) results.push({ label: '⚪ unchecked', path: destUnk, count: unchecked.length });
|
|
@@ -114,6 +94,27 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
|
|
|
114
94
|
const dest = write('', allSessions);
|
|
115
95
|
if (dest) results.push({ label: 'combined', path: dest, count: allSessions.length });
|
|
116
96
|
}
|
|
117
|
-
|
|
118
97
|
return results.length ? results : null;
|
|
119
98
|
}
|
|
99
|
+
|
|
100
|
+
function campaignFileStem(campaignName, cid) {
|
|
101
|
+
const name = (campaignName || cid.slice(0, 8)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || cid.slice(0, 8);
|
|
102
|
+
return `${name}-${cid.slice(0, 8)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// LOCAL autosave — the primary save path. Writes directly from the worker's
|
|
106
|
+
// in-memory results, so a campaign's JSONs are ALWAYS saved even if the dashboard
|
|
107
|
+
// JWT expired mid-run (no server round-trip). Returns [{label,path,count}] or null.
|
|
108
|
+
export function saveSessionsLocal(accounts, cid, { campaignName, out, checkMinutes } = {}) {
|
|
109
|
+
return writeAccounts(accounts, { name: campaignFileStem(campaignName, cid), out, checkMinutes });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Resolve + write a campaign's sessions to a file, fetching from the SERVER.
|
|
113
|
+
// Used by the standalone `export` command (no local buffer available there).
|
|
114
|
+
export async function saveSessions(api, cid, { campaignName, out } = {}) {
|
|
115
|
+
const accounts = await api.exportCampaign(cid);
|
|
116
|
+
if (!accounts || !accounts.length) return null;
|
|
117
|
+
let checkMinutes = false;
|
|
118
|
+
try { const { campaign } = await api.getCampaign(cid); checkMinutes = !!campaign.check_minutes; } catch { /* ignore */ }
|
|
119
|
+
return writeAccounts(accounts, { name: campaignFileStem(campaignName, cid), out, checkMinutes });
|
|
120
|
+
}
|
package/src/worker.js
CHANGED
|
@@ -39,6 +39,10 @@ export class Worker {
|
|
|
39
39
|
this.slots = {};
|
|
40
40
|
this.seq = 0;
|
|
41
41
|
this.stopped = false;
|
|
42
|
+
// Local buffer of every successful session — the source of truth for autosave.
|
|
43
|
+
// We NEVER depend on a server round-trip to save: even if the dashboard JWT
|
|
44
|
+
// expires mid-run, the file still gets written from here.
|
|
45
|
+
this.results = [];
|
|
42
46
|
this.logger = new CampaignLogger({ campaignId, name: name || campaignId, provider: provider.name });
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -69,6 +73,21 @@ export class Worker {
|
|
|
69
73
|
else { this.stats.failed++; this.stats.charges += res.cost || 0; } // failed w/ SMS = charged by provider
|
|
70
74
|
this.onEvent('progress', this.stats);
|
|
71
75
|
this.onEvent('row', res);
|
|
76
|
+
// Buffer every successful session LOCALLY first — autosave reads from here, so a
|
|
77
|
+
// failed/expired server ingest can never lose the JSON the user paid for.
|
|
78
|
+
if (res.status === 'success' && res.session) {
|
|
79
|
+
this.results.push({
|
|
80
|
+
id_no: res.id_no, mobile: res.mobile || '', session: res.session,
|
|
81
|
+
minutes_checked: !!res.minutes_checked,
|
|
82
|
+
coupon_eligible: res.coupon_eligible ?? undefined,
|
|
83
|
+
has_minutes_order: res.has_minutes_order ?? undefined,
|
|
84
|
+
minutes_order_count: res.minutes_order_count ?? undefined,
|
|
85
|
+
minutes_orders: res.minutes_orders ?? undefined,
|
|
86
|
+
linked_email: res.linked_email ?? undefined,
|
|
87
|
+
email_linked: res.email_linked ?? undefined,
|
|
88
|
+
email_reason: res.email_reason ?? undefined,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
72
91
|
try {
|
|
73
92
|
await this.api.ingestAccount(this.cid, {
|
|
74
93
|
id_no: res.id_no, mobile: res.mobile || '', status: res.status, cost: res.cost || 0,
|