viruagent-cli 0.4.1 → 0.5.0
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/bin/index.js +6 -0
- package/package.json +1 -1
- package/src/providers/insta/apiClient.js +104 -47
- package/src/providers/insta/auth.js +62 -11
- package/src/providers/insta/index.js +26 -16
- package/src/providers/insta/session.js +2 -2
- package/src/providers/insta/smartComment.js +8 -8
- package/src/providers/insta/utils.js +5 -5
- package/src/providers/naver/auth.js +19 -19
- package/src/providers/naver/editorConvert.js +16 -16
- package/src/providers/naver/imageUpload.js +7 -7
- package/src/providers/naver/index.js +9 -9
- package/src/providers/naver/session.js +2 -2
- package/src/providers/naver/utils.js +8 -8
- package/src/providers/tistory/auth.js +12 -12
- package/src/providers/tistory/fetchLayer.js +5 -5
- package/src/providers/tistory/imageEnrichment.js +19 -19
- package/src/providers/tistory/imageNormalization.js +1 -1
- package/src/providers/tistory/imageSources.js +5 -5
- package/src/providers/tistory/index.js +15 -15
- package/src/providers/tistory/session.js +3 -3
- package/src/providers/tistory/utils.js +14 -14
- package/src/runner.js +4 -1
- package/src/services/naverApiClient.js +17 -17
- package/src/services/providerManager.js +1 -1
- package/src/services/tistoryApiClient.js +13 -13
package/bin/index.js
CHANGED
|
@@ -212,6 +212,12 @@ analyzePostCmd
|
|
|
212
212
|
.option('--post-id <shortcode>', 'Post shortcode')
|
|
213
213
|
.action((opts) => execute('analyze-post', opts));
|
|
214
214
|
|
|
215
|
+
const resolveChallengeCmd = program
|
|
216
|
+
.command('resolve-challenge')
|
|
217
|
+
.description('Resolve Instagram challenge (auto-verify identity)');
|
|
218
|
+
addProviderOption(resolveChallengeCmd);
|
|
219
|
+
resolveChallengeCmd.action((opts) => execute('resolve-challenge', opts));
|
|
220
|
+
|
|
215
221
|
const rateLimitCmd = program
|
|
216
222
|
.command('rate-limit-status')
|
|
217
223
|
.description('Show current rate limit usage');
|
package/package.json
CHANGED
|
@@ -8,40 +8,40 @@ const randomDelay = (minSec, maxSec) => {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
// ──────────────────────────────────────────────────────────────
|
|
11
|
-
// Instagram
|
|
11
|
+
// Instagram Safe Action Rules (2026, research-based)
|
|
12
12
|
//
|
|
13
|
-
// [
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
13
|
+
// [Hourly limits by account age]
|
|
14
|
+
// New (0~20 days) | Mature (20+ days)
|
|
15
|
+
// Like 15/h | 60/h
|
|
16
|
+
// Comment 5/h | 20/h
|
|
17
|
+
// Follow 15/h | 60/h
|
|
18
|
+
// Unfollow 10/h | 30/h
|
|
19
19
|
// DM 5/h | 50/h
|
|
20
|
-
//
|
|
20
|
+
// Publish 3/h | 10/h
|
|
21
21
|
//
|
|
22
|
-
// [
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// [Daily limits]
|
|
23
|
+
// Like 500/day Comment 100/day Follow 250/day
|
|
24
|
+
// Unfollow 200/day DM 30/day Publish 25/day
|
|
25
25
|
//
|
|
26
|
-
// [
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// [Minimum action intervals (new accounts)]
|
|
27
|
+
// Like: 20~40s | Comment: 300~420s (5~7min) | Follow: 60~120s
|
|
28
|
+
// Unfollow: 60~120s | DM: 120~300s | Publish: 60~120s
|
|
29
29
|
//
|
|
30
|
-
// [
|
|
31
|
-
// -
|
|
32
|
-
// -
|
|
33
|
-
// -
|
|
34
|
-
// -
|
|
35
|
-
// -
|
|
30
|
+
// [Notes]
|
|
31
|
+
// - Max 15 total actions/hour (new) / 40 (mature)
|
|
32
|
+
// - Uniform intervals trigger bot detection → random delay required
|
|
33
|
+
// - Repeated actions on the same user are prohibited
|
|
34
|
+
// - Challenge requires manual verification in the browser
|
|
35
|
+
// - Wait 24~48 hours after a challenge is recommended
|
|
36
36
|
// ──────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
38
|
const DELAY = {
|
|
39
|
-
like: [20, 40], // 20~
|
|
40
|
-
comment: [300, 420], // 5~
|
|
41
|
-
follow: [60, 120], // 1~
|
|
42
|
-
unfollow: [60, 120], // 1~
|
|
43
|
-
dm: [120, 300], // 2~
|
|
44
|
-
publish: [60, 120], // 1~
|
|
39
|
+
like: [20, 40], // 20~40s
|
|
40
|
+
comment: [300, 420], // 5~7min
|
|
41
|
+
follow: [60, 120], // 1~2min
|
|
42
|
+
unfollow: [60, 120], // 1~2min
|
|
43
|
+
dm: [120, 300], // 2~5min
|
|
44
|
+
publish: [60, 120], // 1~2min
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const HOURLY_LIMIT = {
|
|
@@ -69,7 +69,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
69
69
|
let cachedUserId = null;
|
|
70
70
|
let countersCache = null;
|
|
71
71
|
|
|
72
|
-
// ──
|
|
72
|
+
// ── Session file-based Rate Limit counters ──
|
|
73
73
|
|
|
74
74
|
const loadCounters = () => {
|
|
75
75
|
if (countersCache) return countersCache;
|
|
@@ -90,7 +90,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
90
90
|
if (!userId || !countersCache) return;
|
|
91
91
|
saveRateLimits(sessionPath, userId, countersCache);
|
|
92
92
|
} catch {
|
|
93
|
-
//
|
|
93
|
+
// Save failure does not affect operation
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
|
|
@@ -112,10 +112,10 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
112
112
|
const dailyMax = DAILY_LIMIT[type];
|
|
113
113
|
if (hourlyMax && c.hourly >= hourlyMax) {
|
|
114
114
|
const waitMin = Math.ceil((3600000 - (Date.now() - c.hourStart)) / 60000);
|
|
115
|
-
throw new Error(`hourly_limit: ${type}
|
|
115
|
+
throw new Error(`hourly_limit: ${type} exceeded hourly limit of ${hourlyMax}. Retry in ${waitMin} minutes.`);
|
|
116
116
|
}
|
|
117
117
|
if (dailyMax && c.daily >= dailyMax) {
|
|
118
|
-
throw new Error(`daily_limit: ${type}
|
|
118
|
+
throw new Error(`daily_limit: ${type} exceeded daily limit of ${dailyMax}. Try again tomorrow.`);
|
|
119
119
|
}
|
|
120
120
|
};
|
|
121
121
|
|
|
@@ -130,11 +130,11 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
130
130
|
if (cachedCookies) return cachedCookies;
|
|
131
131
|
const cookies = loadInstaSession(sessionPath);
|
|
132
132
|
if (!cookies) {
|
|
133
|
-
throw new Error('
|
|
133
|
+
throw new Error('No session file found. Please log in first.');
|
|
134
134
|
}
|
|
135
135
|
const sessionid = cookies.find((c) => c.name === 'sessionid');
|
|
136
136
|
if (!sessionid?.value) {
|
|
137
|
-
throw new Error('
|
|
137
|
+
throw new Error('No valid cookies in session. Please log in again.');
|
|
138
138
|
}
|
|
139
139
|
cachedCookies = cookies;
|
|
140
140
|
return cookies;
|
|
@@ -172,17 +172,44 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
172
172
|
if (res.status === 302 || res.status === 301) {
|
|
173
173
|
const location = res.headers.get('location') || '';
|
|
174
174
|
if (location.includes('/accounts/login')) {
|
|
175
|
-
throw new Error('
|
|
175
|
+
throw new Error('Session expired. Please log in again.');
|
|
176
|
+
}
|
|
177
|
+
if (location.includes('/challenge')) {
|
|
178
|
+
// Attempt automatic challenge resolution
|
|
179
|
+
const resolved = await resolveChallenge();
|
|
180
|
+
if (resolved) {
|
|
181
|
+
// Retry original request after resolution
|
|
182
|
+
return fetch(url, { ...options, headers, redirect: 'manual' });
|
|
183
|
+
}
|
|
184
|
+
throw new Error('challenge_required: Identity verification required. Please complete it manually in the browser.');
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Redirect occurred: ${res.status} → ${location}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Handle challenge_required JSON response
|
|
190
|
+
if (res.status === 400 && !options.allowError) {
|
|
191
|
+
const cloned = res.clone();
|
|
192
|
+
try {
|
|
193
|
+
const data = await cloned.json();
|
|
194
|
+
if (data.message === 'challenge_required') {
|
|
195
|
+
const resolved = await resolveChallenge();
|
|
196
|
+
if (resolved) {
|
|
197
|
+
return fetch(url, { ...options, headers, redirect: 'manual' });
|
|
198
|
+
}
|
|
199
|
+
throw new Error('challenge_required: Identity verification required.');
|
|
200
|
+
}
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (e.message.includes('challenge_required')) throw e;
|
|
203
|
+
// Ignore JSON parse failure and continue original flow
|
|
176
204
|
}
|
|
177
|
-
throw new Error(`리다이렉트 발생: ${res.status} → ${location}`);
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
if (res.status === 401 || res.status === 403) {
|
|
181
|
-
throw new Error(
|
|
208
|
+
throw new Error(`Authentication error (${res.status}). Please log in again.`);
|
|
182
209
|
}
|
|
183
210
|
|
|
184
211
|
if (!res.ok && !options.allowError) {
|
|
185
|
-
throw new Error(`Instagram API
|
|
212
|
+
throw new Error(`Instagram API error: ${res.status} ${res.statusText}`);
|
|
186
213
|
}
|
|
187
214
|
|
|
188
215
|
return res;
|
|
@@ -194,7 +221,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
194
221
|
);
|
|
195
222
|
const data = await res.json();
|
|
196
223
|
const user = data?.data?.user;
|
|
197
|
-
if (!user) throw new Error(
|
|
224
|
+
if (!user) throw new Error(`Profile not found: ${username}`);
|
|
198
225
|
return {
|
|
199
226
|
id: user.id,
|
|
200
227
|
username: user.username,
|
|
@@ -295,7 +322,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
295
322
|
has_threaded_comments: true,
|
|
296
323
|
});
|
|
297
324
|
const media = data?.data?.xdt_shortcode_media || data?.data?.shortcode_media;
|
|
298
|
-
if (!media) throw new Error(
|
|
325
|
+
if (!media) throw new Error(`Post not found: ${shortcode}`);
|
|
299
326
|
return {
|
|
300
327
|
id: media.id,
|
|
301
328
|
code: media.shortcode,
|
|
@@ -316,6 +343,35 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
316
343
|
};
|
|
317
344
|
};
|
|
318
345
|
|
|
346
|
+
// ── Automatic Challenge Resolution ──
|
|
347
|
+
|
|
348
|
+
const resolveChallenge = async () => {
|
|
349
|
+
try {
|
|
350
|
+
const cookies = getCookies();
|
|
351
|
+
const res = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
headers: {
|
|
354
|
+
'User-Agent': USER_AGENT,
|
|
355
|
+
'X-IG-App-ID': IG_APP_ID,
|
|
356
|
+
'X-CSRFToken': getCsrfToken(),
|
|
357
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
358
|
+
'X-Instagram-AJAX': '1',
|
|
359
|
+
Referer: 'https://www.instagram.com/challenge/',
|
|
360
|
+
Origin: 'https://www.instagram.com',
|
|
361
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
362
|
+
Cookie: cookiesToHeader(cookies),
|
|
363
|
+
},
|
|
364
|
+
body: 'choice=0',
|
|
365
|
+
redirect: 'manual',
|
|
366
|
+
});
|
|
367
|
+
if (!res.ok) return false;
|
|
368
|
+
const data = await res.json();
|
|
369
|
+
return data.status === 'ok';
|
|
370
|
+
} catch {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
319
375
|
const parseLikeResponse = async (res) => {
|
|
320
376
|
if (res.ok) return res.json();
|
|
321
377
|
if (res.status === 400) {
|
|
@@ -323,17 +379,17 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
323
379
|
if (data.spam) {
|
|
324
380
|
throw new Error(`rate_limit: ${data.feedback_title || 'Try Again Later'}`);
|
|
325
381
|
}
|
|
326
|
-
//
|
|
382
|
+
// Already liked/unliked state
|
|
327
383
|
return { status: 'already', message: data.message };
|
|
328
384
|
}
|
|
329
|
-
throw new Error(`Instagram API
|
|
385
|
+
throw new Error(`Instagram API error: ${res.status}`);
|
|
330
386
|
};
|
|
331
387
|
|
|
332
388
|
const withDelay = async (type, fn) => {
|
|
333
|
-
//
|
|
389
|
+
// Check limits
|
|
334
390
|
checkLimit(type);
|
|
335
391
|
|
|
336
|
-
//
|
|
392
|
+
// Random delay
|
|
337
393
|
const [min, max] = DELAY[type] || [20, 40];
|
|
338
394
|
const elapsed = (Date.now() - lastActionTime) / 1000;
|
|
339
395
|
if (lastActionTime > 0 && elapsed < min) {
|
|
@@ -519,7 +575,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
519
575
|
);
|
|
520
576
|
const data = await res.json();
|
|
521
577
|
if (data.status !== 'ok') {
|
|
522
|
-
throw new Error(
|
|
578
|
+
throw new Error(`Image upload failed: ${data.message || 'unknown'}`);
|
|
523
579
|
}
|
|
524
580
|
return uploadId;
|
|
525
581
|
};
|
|
@@ -545,7 +601,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
545
601
|
});
|
|
546
602
|
const data = await res.json();
|
|
547
603
|
if (data.status !== 'ok') {
|
|
548
|
-
throw new Error(
|
|
604
|
+
throw new Error(`Post creation failed: ${data.message || 'unknown'}`);
|
|
549
605
|
}
|
|
550
606
|
return {
|
|
551
607
|
id: data.media?.pk,
|
|
@@ -561,10 +617,10 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
561
617
|
imageBuffer = fs.readFileSync(imagePath);
|
|
562
618
|
} else if (imageUrl) {
|
|
563
619
|
const res = await fetch(imageUrl);
|
|
564
|
-
if (!res.ok) throw new Error(
|
|
620
|
+
if (!res.ok) throw new Error(`Image download failed: ${res.status}`);
|
|
565
621
|
imageBuffer = Buffer.from(await res.arrayBuffer());
|
|
566
622
|
} else {
|
|
567
|
-
throw new Error('imageUrl
|
|
623
|
+
throw new Error('Either imageUrl or imagePath is required.');
|
|
568
624
|
}
|
|
569
625
|
|
|
570
626
|
const uploadId = await uploadPhoto(imageBuffer);
|
|
@@ -611,6 +667,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
611
667
|
configurePost,
|
|
612
668
|
publishPost,
|
|
613
669
|
deletePost,
|
|
670
|
+
resolveChallenge,
|
|
614
671
|
resetState,
|
|
615
672
|
getRateLimitStatus: () => {
|
|
616
673
|
const status = {};
|
|
@@ -619,7 +676,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
619
676
|
status[type] = {
|
|
620
677
|
hourly: `${c.hourly}/${HOURLY_LIMIT[type]}`,
|
|
621
678
|
daily: `${c.daily}/${DAILY_LIMIT[type]}`,
|
|
622
|
-
delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}
|
|
679
|
+
delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}s`,
|
|
623
680
|
};
|
|
624
681
|
}
|
|
625
682
|
return status;
|
|
@@ -50,12 +50,12 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
50
50
|
|
|
51
51
|
if (!resolvedUsername || !resolvedPassword) {
|
|
52
52
|
throw new Error(
|
|
53
|
-
'
|
|
54
|
-
'
|
|
53
|
+
'Instagram login requires username/password. ' +
|
|
54
|
+
'Please set the INSTA_USERNAME / INSTA_PASSWORD environment variables.',
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
// Step 1: GET login page -> csrftoken + mid
|
|
58
|
+
// Step 1: GET login page -> obtain csrftoken + mid cookies
|
|
59
59
|
const initRes = await fetch('https://www.instagram.com/accounts/login/', {
|
|
60
60
|
headers: { 'User-Agent': USER_AGENT },
|
|
61
61
|
redirect: 'manual',
|
|
@@ -64,7 +64,7 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
64
64
|
|
|
65
65
|
const csrfCookie = cookies.find((c) => c.name === 'csrftoken');
|
|
66
66
|
if (!csrfCookie) {
|
|
67
|
-
throw new Error('
|
|
67
|
+
throw new Error('Failed to retrieve csrftoken from the Instagram login page.');
|
|
68
68
|
}
|
|
69
69
|
const csrfToken = csrfCookie.value;
|
|
70
70
|
const cookieHeader = cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
@@ -99,24 +99,75 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
99
99
|
|
|
100
100
|
const loginData = await loginRes.json();
|
|
101
101
|
|
|
102
|
-
if (loginData.checkpoint_url) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (loginData.checkpoint_url || loginData.message === 'challenge_required') {
|
|
103
|
+
// Attempt automatic challenge resolution (choice=0 = "This was me")
|
|
104
|
+
const challengeRes = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: {
|
|
107
|
+
'User-Agent': USER_AGENT,
|
|
108
|
+
'X-CSRFToken': csrfToken,
|
|
109
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
110
|
+
'X-Instagram-AJAX': '1',
|
|
111
|
+
'X-IG-App-ID': IG_APP_ID,
|
|
112
|
+
Referer: 'https://www.instagram.com/challenge/',
|
|
113
|
+
Origin: 'https://www.instagram.com',
|
|
114
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
115
|
+
Cookie: cookieHeader,
|
|
116
|
+
},
|
|
117
|
+
body: 'choice=0',
|
|
118
|
+
redirect: 'manual',
|
|
119
|
+
});
|
|
120
|
+
const challengeData = await challengeRes.json().catch(() => ({}));
|
|
121
|
+
if (challengeData.status !== 'ok') {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'challenge_required: Automatic resolution failed. Please complete identity verification in the browser. ' +
|
|
124
|
+
(loginData.checkpoint_url || ''),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
// Re-login after challenge resolution
|
|
128
|
+
const retryRes = await fetch('https://www.instagram.com/api/v1/web/accounts/login/ajax/', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'User-Agent': USER_AGENT,
|
|
132
|
+
'X-CSRFToken': csrfToken,
|
|
133
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
134
|
+
'X-IG-App-ID': IG_APP_ID,
|
|
135
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
136
|
+
Referer: 'https://www.instagram.com/accounts/login/',
|
|
137
|
+
Origin: 'https://www.instagram.com',
|
|
138
|
+
Cookie: cookieHeader,
|
|
139
|
+
},
|
|
140
|
+
body: body.toString(),
|
|
141
|
+
redirect: 'manual',
|
|
142
|
+
});
|
|
143
|
+
const retryCookies = parseCookiesFromHeaders(retryRes.headers);
|
|
144
|
+
cookies = mergeCookies(cookies, retryCookies);
|
|
145
|
+
const retryData = await retryRes.json().catch(() => ({}));
|
|
146
|
+
if (retryData.authenticated) {
|
|
147
|
+
saveInstaSession(sessionPath, cookies);
|
|
148
|
+
return {
|
|
149
|
+
provider: 'insta',
|
|
150
|
+
loggedIn: true,
|
|
151
|
+
userId: retryData.userId || null,
|
|
152
|
+
username: resolvedUsername,
|
|
153
|
+
sessionPath,
|
|
154
|
+
challengeResolved: true,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
106
157
|
}
|
|
107
158
|
|
|
108
159
|
if (loginData.two_factor_required) {
|
|
109
160
|
throw new Error(
|
|
110
|
-
'
|
|
161
|
+
'Two-factor authentication (2FA) is required. Please complete verification in the browser first.',
|
|
111
162
|
);
|
|
112
163
|
}
|
|
113
164
|
|
|
114
165
|
if (!loginData.authenticated) {
|
|
115
166
|
const reason = loginData.message || loginData.status || 'unknown';
|
|
116
|
-
throw new Error(
|
|
167
|
+
throw new Error(`Instagram login failed: ${reason}`);
|
|
117
168
|
}
|
|
118
169
|
|
|
119
|
-
//
|
|
170
|
+
// Save session
|
|
120
171
|
saveInstaSession(sessionPath, cookies);
|
|
121
172
|
|
|
122
173
|
return {
|
|
@@ -52,8 +52,8 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
52
52
|
|
|
53
53
|
if (!resolved.username || !resolved.password) {
|
|
54
54
|
throw new Error(
|
|
55
|
-
'
|
|
56
|
-
'
|
|
55
|
+
'Instagram login requires username/password. ' +
|
|
56
|
+
'Please set the INSTA_USERNAME / INSTA_PASSWORD environment variables.',
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -73,7 +73,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
73
73
|
async getProfile({ username } = {}) {
|
|
74
74
|
return withProviderSession(async () => {
|
|
75
75
|
if (!username) {
|
|
76
|
-
throw new Error('username
|
|
76
|
+
throw new Error('username is required.');
|
|
77
77
|
}
|
|
78
78
|
const profile = await instaApi.getProfile(username);
|
|
79
79
|
return {
|
|
@@ -99,7 +99,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
99
99
|
async listPosts({ username, limit = 12 } = {}) {
|
|
100
100
|
return withProviderSession(async () => {
|
|
101
101
|
if (!username) {
|
|
102
|
-
throw new Error('username
|
|
102
|
+
throw new Error('username is required.');
|
|
103
103
|
}
|
|
104
104
|
const posts = await instaApi.getUserPosts(username, limit);
|
|
105
105
|
return {
|
|
@@ -120,7 +120,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
120
120
|
provider: 'insta',
|
|
121
121
|
mode: 'post',
|
|
122
122
|
status: 'invalid_post_id',
|
|
123
|
-
message: 'postId(shortcode)
|
|
123
|
+
message: 'postId (shortcode) is required.',
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
const post = await instaApi.getPostDetail(shortcode);
|
|
@@ -134,7 +134,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
134
134
|
|
|
135
135
|
async follow({ username } = {}) {
|
|
136
136
|
return withProviderSession(async () => {
|
|
137
|
-
if (!username) throw new Error('username
|
|
137
|
+
if (!username) throw new Error('username is required.');
|
|
138
138
|
const profile = await instaApi.getProfile(username);
|
|
139
139
|
const result = await instaApi.followUser(profile.id);
|
|
140
140
|
return {
|
|
@@ -151,7 +151,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
151
151
|
|
|
152
152
|
async unfollow({ username } = {}) {
|
|
153
153
|
return withProviderSession(async () => {
|
|
154
|
-
if (!username) throw new Error('username
|
|
154
|
+
if (!username) throw new Error('username is required.');
|
|
155
155
|
const profile = await instaApi.getProfile(username);
|
|
156
156
|
const result = await instaApi.unfollowUser(profile.id);
|
|
157
157
|
return {
|
|
@@ -168,7 +168,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
168
168
|
async like({ postId } = {}) {
|
|
169
169
|
return withProviderSession(async () => {
|
|
170
170
|
const shortcode = String(postId || '').trim();
|
|
171
|
-
if (!shortcode) throw new Error('postId(shortcode)
|
|
171
|
+
if (!shortcode) throw new Error('postId (shortcode) is required.');
|
|
172
172
|
const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
|
|
173
173
|
const result = await instaApi.likePost(mediaId);
|
|
174
174
|
return { provider: 'insta', mode: 'like', postId: shortcode, status: result.status };
|
|
@@ -178,7 +178,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
178
178
|
async unlike({ postId } = {}) {
|
|
179
179
|
return withProviderSession(async () => {
|
|
180
180
|
const shortcode = String(postId || '').trim();
|
|
181
|
-
if (!shortcode) throw new Error('postId(shortcode)
|
|
181
|
+
if (!shortcode) throw new Error('postId (shortcode) is required.');
|
|
182
182
|
const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
|
|
183
183
|
const result = await instaApi.unlikePost(mediaId);
|
|
184
184
|
return { provider: 'insta', mode: 'unlike', postId: shortcode, status: result.status };
|
|
@@ -187,7 +187,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
187
187
|
|
|
188
188
|
async likeComment({ commentId } = {}) {
|
|
189
189
|
return withProviderSession(async () => {
|
|
190
|
-
if (!commentId) throw new Error('commentId
|
|
190
|
+
if (!commentId) throw new Error('commentId is required.');
|
|
191
191
|
const result = await instaApi.likeComment(commentId);
|
|
192
192
|
return { provider: 'insta', mode: 'likeComment', commentId, status: result.status };
|
|
193
193
|
});
|
|
@@ -195,7 +195,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
195
195
|
|
|
196
196
|
async unlikeComment({ commentId } = {}) {
|
|
197
197
|
return withProviderSession(async () => {
|
|
198
|
-
if (!commentId) throw new Error('commentId
|
|
198
|
+
if (!commentId) throw new Error('commentId is required.');
|
|
199
199
|
const result = await instaApi.unlikeComment(commentId);
|
|
200
200
|
return { provider: 'insta', mode: 'unlikeComment', commentId, status: result.status };
|
|
201
201
|
});
|
|
@@ -206,10 +206,10 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
206
206
|
const shortcode = String(postId || '').trim();
|
|
207
207
|
const commentText = String(text || '').trim();
|
|
208
208
|
if (!shortcode) {
|
|
209
|
-
throw new Error('postId(shortcode)
|
|
209
|
+
throw new Error('postId (shortcode) is required.');
|
|
210
210
|
}
|
|
211
211
|
if (!commentText) {
|
|
212
|
-
throw new Error('
|
|
212
|
+
throw new Error('Comment text is required.');
|
|
213
213
|
}
|
|
214
214
|
const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
|
|
215
215
|
const result = await instaApi.addComment(mediaId, commentText);
|
|
@@ -228,7 +228,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
228
228
|
async publish({ imageUrl, imagePath, caption = '' } = {}) {
|
|
229
229
|
return withProviderSession(async () => {
|
|
230
230
|
if (!imageUrl && !imagePath) {
|
|
231
|
-
throw new Error('imageUrl
|
|
231
|
+
throw new Error('Either imageUrl or imagePath is required.');
|
|
232
232
|
}
|
|
233
233
|
const result = await instaApi.publishPost({ imageUrl, imagePath, caption });
|
|
234
234
|
return {
|
|
@@ -243,7 +243,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
243
243
|
return withProviderSession(async () => {
|
|
244
244
|
const shortcode = String(postId || '').trim();
|
|
245
245
|
if (!shortcode) {
|
|
246
|
-
throw new Error('postId(shortcode)
|
|
246
|
+
throw new Error('postId (shortcode) is required.');
|
|
247
247
|
}
|
|
248
248
|
const analysis = await smart.analyzePost({ shortcode });
|
|
249
249
|
return {
|
|
@@ -258,7 +258,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
258
258
|
return withProviderSession(async () => {
|
|
259
259
|
const shortcode = String(postId || '').trim();
|
|
260
260
|
if (!shortcode) {
|
|
261
|
-
throw new Error('postId(shortcode)
|
|
261
|
+
throw new Error('postId (shortcode) is required.');
|
|
262
262
|
}
|
|
263
263
|
const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
|
|
264
264
|
const result = await instaApi.deletePost(mediaId);
|
|
@@ -271,6 +271,16 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
271
271
|
});
|
|
272
272
|
},
|
|
273
273
|
|
|
274
|
+
async resolveChallenge() {
|
|
275
|
+
const resolved = await instaApi.resolveChallenge();
|
|
276
|
+
return {
|
|
277
|
+
provider: 'insta',
|
|
278
|
+
mode: 'resolveChallenge',
|
|
279
|
+
resolved,
|
|
280
|
+
message: resolved ? 'Challenge resolved successfully.' : 'Challenge resolution failed. Please handle it manually in the browser.',
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
|
|
274
284
|
rateLimitStatus() {
|
|
275
285
|
return {
|
|
276
286
|
provider: 'insta',
|
|
@@ -33,7 +33,7 @@ const loadInstaSession = (sessionPath) => {
|
|
|
33
33
|
return Array.isArray(raw?.cookies) ? raw.cookies : null;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
// ── Rate Limit
|
|
36
|
+
// ── Rate Limit persistence (per userId) ──
|
|
37
37
|
|
|
38
38
|
const loadRateLimits = (sessionPath, userId) => {
|
|
39
39
|
const raw = readSessionFile(sessionPath);
|
|
@@ -92,7 +92,7 @@ const createInstaWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
if (!loginResult.loggedIn) {
|
|
95
|
-
throw new Error(loginResult.message || '
|
|
95
|
+
throw new Error(loginResult.message || 'Login status could not be confirmed after session refresh.');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
return fn();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const createSmartComment = (instaApi) => {
|
|
2
2
|
const analyzePost = async ({ shortcode }) => {
|
|
3
|
-
// 1.
|
|
3
|
+
// 1. Post detail
|
|
4
4
|
const post = await instaApi.getPostDetail(shortcode);
|
|
5
5
|
const caption = post.caption || '';
|
|
6
6
|
const isVideo = post.isVideo;
|
|
@@ -8,15 +8,15 @@ const createSmartComment = (instaApi) => {
|
|
|
8
8
|
const thumbnailUrl = post.imageUrl;
|
|
9
9
|
const ownerUsername = post.owner?.username || '';
|
|
10
10
|
|
|
11
|
-
// 2.
|
|
11
|
+
// 2. Owner profile
|
|
12
12
|
let ownerProfile = null;
|
|
13
13
|
try {
|
|
14
14
|
ownerProfile = await instaApi.getProfile(ownerUsername);
|
|
15
15
|
} catch {
|
|
16
|
-
//
|
|
16
|
+
// Ignore failures (e.g., private account)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// 3.
|
|
19
|
+
// 3. Thumbnail image base64 (for Claude Code Vision)
|
|
20
20
|
let thumbnailBase64 = null;
|
|
21
21
|
let thumbnailMediaType = 'image/jpeg';
|
|
22
22
|
if (thumbnailUrl) {
|
|
@@ -29,15 +29,15 @@ const createSmartComment = (instaApi) => {
|
|
|
29
29
|
if (ct) thumbnailMediaType = ct;
|
|
30
30
|
}
|
|
31
31
|
} catch {
|
|
32
|
-
//
|
|
32
|
+
// Proceed with caption only on failure
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const contentType = isVideo
|
|
37
|
-
? '
|
|
37
|
+
? 'video (reel)'
|
|
38
38
|
: mediaType?.includes('Sidecar')
|
|
39
|
-
? '
|
|
40
|
-
: '
|
|
39
|
+
? 'carousel (multiple images)'
|
|
40
|
+
: 'photo';
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
43
|
shortcode,
|
|
@@ -10,10 +10,10 @@ const readInstaCredentials = () => {
|
|
|
10
10
|
const parseInstaSessionError = (error) => {
|
|
11
11
|
const message = String(error?.message || '').toLowerCase();
|
|
12
12
|
return [
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
13
|
+
'no session file found',
|
|
14
|
+
'no valid cookies in session',
|
|
15
|
+
'session expired',
|
|
16
|
+
'login required',
|
|
17
17
|
'login_required',
|
|
18
18
|
'checkpoint_required',
|
|
19
19
|
'401',
|
|
@@ -22,7 +22,7 @@ const parseInstaSessionError = (error) => {
|
|
|
22
22
|
].some((token) => message.includes(token.toLowerCase()));
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
const buildLoginErrorMessage = (error) => String(error?.message || '
|
|
25
|
+
const buildLoginErrorMessage = (error) => String(error?.message || 'Session validation failed.');
|
|
26
26
|
|
|
27
27
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
28
|
|