viruagent-cli 0.4.2 → 0.5.1
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 +26 -0
- package/package.json +1 -1
- package/src/providers/insta/apiClient.js +92 -54
- package/src/providers/insta/auth.js +10 -10
- package/src/providers/insta/index.js +215 -17
- 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 +22 -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
|
@@ -204,6 +204,32 @@ unlikeCommentCmd
|
|
|
204
204
|
.option('--comment-id <id>', 'Comment ID')
|
|
205
205
|
.action((opts) => execute('unlike-comment', opts));
|
|
206
206
|
|
|
207
|
+
const sendDmCmd = program
|
|
208
|
+
.command('send-dm')
|
|
209
|
+
.description('Send a direct message to a user');
|
|
210
|
+
addProviderOption(sendDmCmd);
|
|
211
|
+
sendDmCmd
|
|
212
|
+
.option('--username <username>', 'Recipient username')
|
|
213
|
+
.option('--thread-id <threadId>', 'Existing thread ID')
|
|
214
|
+
.option('--text <message>', 'Message text')
|
|
215
|
+
.action((opts) => execute('send-dm', opts));
|
|
216
|
+
|
|
217
|
+
const listMessagesCmd = program
|
|
218
|
+
.command('list-messages')
|
|
219
|
+
.description('List messages in a DM thread (via browser)');
|
|
220
|
+
addProviderOption(listMessagesCmd);
|
|
221
|
+
listMessagesCmd
|
|
222
|
+
.option('--thread-id <threadId>', 'Thread ID')
|
|
223
|
+
.action((opts) => execute('list-messages', opts));
|
|
224
|
+
|
|
225
|
+
const listCommentsCmd = program
|
|
226
|
+
.command('list-comments')
|
|
227
|
+
.description('List comments on a post');
|
|
228
|
+
addProviderOption(listCommentsCmd);
|
|
229
|
+
listCommentsCmd
|
|
230
|
+
.option('--post-id <shortcode>', 'Post shortcode')
|
|
231
|
+
.action((opts) => execute('list-comments', opts));
|
|
232
|
+
|
|
207
233
|
const analyzePostCmd = program
|
|
208
234
|
.command('analyze-post')
|
|
209
235
|
.description('Analyze a post (thumbnail + caption + profile)');
|
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,21 +172,21 @@ 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
176
|
}
|
|
177
177
|
if (location.includes('/challenge')) {
|
|
178
|
-
//
|
|
178
|
+
// Attempt automatic challenge resolution
|
|
179
179
|
const resolved = await resolveChallenge();
|
|
180
180
|
if (resolved) {
|
|
181
|
-
//
|
|
181
|
+
// Retry original request after resolution
|
|
182
182
|
return fetch(url, { ...options, headers, redirect: 'manual' });
|
|
183
183
|
}
|
|
184
|
-
throw new Error('challenge_required:
|
|
184
|
+
throw new Error('challenge_required: Identity verification required. Please complete it manually in the browser.');
|
|
185
185
|
}
|
|
186
|
-
throw new Error(
|
|
186
|
+
throw new Error(`Redirect occurred: ${res.status} → ${location}`);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// challenge_required JSON
|
|
189
|
+
// Handle challenge_required JSON response
|
|
190
190
|
if (res.status === 400 && !options.allowError) {
|
|
191
191
|
const cloned = res.clone();
|
|
192
192
|
try {
|
|
@@ -196,20 +196,20 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
196
196
|
if (resolved) {
|
|
197
197
|
return fetch(url, { ...options, headers, redirect: 'manual' });
|
|
198
198
|
}
|
|
199
|
-
throw new Error('challenge_required:
|
|
199
|
+
throw new Error('challenge_required: Identity verification required.');
|
|
200
200
|
}
|
|
201
201
|
} catch (e) {
|
|
202
202
|
if (e.message.includes('challenge_required')) throw e;
|
|
203
|
-
// JSON
|
|
203
|
+
// Ignore JSON parse failure and continue original flow
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
if (res.status === 401 || res.status === 403) {
|
|
208
|
-
throw new Error(
|
|
208
|
+
throw new Error(`Authentication error (${res.status}). Please log in again.`);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
if (!res.ok && !options.allowError) {
|
|
212
|
-
throw new Error(`Instagram API
|
|
212
|
+
throw new Error(`Instagram API error: ${res.status} ${res.statusText}`);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
return res;
|
|
@@ -221,7 +221,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
221
221
|
);
|
|
222
222
|
const data = await res.json();
|
|
223
223
|
const user = data?.data?.user;
|
|
224
|
-
if (!user) throw new Error(
|
|
224
|
+
if (!user) throw new Error(`Profile not found: ${username}`);
|
|
225
225
|
return {
|
|
226
226
|
id: user.id,
|
|
227
227
|
username: user.username,
|
|
@@ -322,7 +322,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
322
322
|
has_threaded_comments: true,
|
|
323
323
|
});
|
|
324
324
|
const media = data?.data?.xdt_shortcode_media || data?.data?.shortcode_media;
|
|
325
|
-
if (!media) throw new Error(
|
|
325
|
+
if (!media) throw new Error(`Post not found: ${shortcode}`);
|
|
326
326
|
return {
|
|
327
327
|
id: media.id,
|
|
328
328
|
code: media.shortcode,
|
|
@@ -343,7 +343,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
343
343
|
};
|
|
344
344
|
};
|
|
345
345
|
|
|
346
|
-
// ── Challenge
|
|
346
|
+
// ── Automatic Challenge Resolution ──
|
|
347
347
|
|
|
348
348
|
const resolveChallenge = async () => {
|
|
349
349
|
try {
|
|
@@ -379,17 +379,17 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
379
379
|
if (data.spam) {
|
|
380
380
|
throw new Error(`rate_limit: ${data.feedback_title || 'Try Again Later'}`);
|
|
381
381
|
}
|
|
382
|
-
//
|
|
382
|
+
// Already liked/unliked state
|
|
383
383
|
return { status: 'already', message: data.message };
|
|
384
384
|
}
|
|
385
|
-
throw new Error(`Instagram API
|
|
385
|
+
throw new Error(`Instagram API error: ${res.status}`);
|
|
386
386
|
};
|
|
387
387
|
|
|
388
388
|
const withDelay = async (type, fn) => {
|
|
389
|
-
//
|
|
389
|
+
// Check limits
|
|
390
390
|
checkLimit(type);
|
|
391
391
|
|
|
392
|
-
//
|
|
392
|
+
// Random delay
|
|
393
393
|
const [min, max] = DELAY[type] || [20, 40];
|
|
394
394
|
const elapsed = (Date.now() - lastActionTime) / 1000;
|
|
395
395
|
if (lastActionTime > 0 && elapsed < min) {
|
|
@@ -545,6 +545,43 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
545
545
|
return res.json();
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
+
const sendDm = (recipientUserId, text) => withDelay('dm', async () => {
|
|
549
|
+
const cookies = getCookies();
|
|
550
|
+
const csrf = getCsrfToken();
|
|
551
|
+
const body = new URLSearchParams({
|
|
552
|
+
action: 'send_item',
|
|
553
|
+
recipient_users: `[[${recipientUserId}]]`,
|
|
554
|
+
client_context: `6${Date.now()}_${Math.floor(Math.random() * 1000000000)}`,
|
|
555
|
+
offline_threading_id: Date.now().toString(),
|
|
556
|
+
text,
|
|
557
|
+
});
|
|
558
|
+
const res = await fetch('https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/', {
|
|
559
|
+
method: 'POST',
|
|
560
|
+
headers: {
|
|
561
|
+
'User-Agent': 'Instagram 317.0.0.34.109 Android (30/11; 420dpi; 1080x2220; samsung; SM-A515F; a51; exynos9611; en_US; 562940699)',
|
|
562
|
+
'X-IG-App-ID': '567067343352427',
|
|
563
|
+
'X-CSRFToken': csrf,
|
|
564
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
565
|
+
Cookie: cookiesToHeader(cookies),
|
|
566
|
+
},
|
|
567
|
+
body: body.toString(),
|
|
568
|
+
redirect: 'manual',
|
|
569
|
+
});
|
|
570
|
+
const data = await res.json();
|
|
571
|
+
if (data.status !== 'ok') {
|
|
572
|
+
const errCode = data.content?.error_code;
|
|
573
|
+
if (errCode === 4415001) {
|
|
574
|
+
throw new Error('DM restricted: Account DM feature is limited. Please open Instagram in browser, go to Direct Messages, and resolve any pending prompts or challenges.');
|
|
575
|
+
}
|
|
576
|
+
throw new Error(`DM failed: ${JSON.stringify(data)}`);
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
status: data.status,
|
|
580
|
+
threadId: data.payload?.thread_id || null,
|
|
581
|
+
itemId: data.payload?.item_id || null,
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
|
|
548
585
|
const getMediaIdFromShortcode = async (shortcode) => {
|
|
549
586
|
const detail = await getPostDetail(shortcode);
|
|
550
587
|
return detail.id;
|
|
@@ -575,7 +612,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
575
612
|
);
|
|
576
613
|
const data = await res.json();
|
|
577
614
|
if (data.status !== 'ok') {
|
|
578
|
-
throw new Error(
|
|
615
|
+
throw new Error(`Image upload failed: ${data.message || 'unknown'}`);
|
|
579
616
|
}
|
|
580
617
|
return uploadId;
|
|
581
618
|
};
|
|
@@ -601,7 +638,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
601
638
|
});
|
|
602
639
|
const data = await res.json();
|
|
603
640
|
if (data.status !== 'ok') {
|
|
604
|
-
throw new Error(
|
|
641
|
+
throw new Error(`Post creation failed: ${data.message || 'unknown'}`);
|
|
605
642
|
}
|
|
606
643
|
return {
|
|
607
644
|
id: data.media?.pk,
|
|
@@ -617,10 +654,10 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
617
654
|
imageBuffer = fs.readFileSync(imagePath);
|
|
618
655
|
} else if (imageUrl) {
|
|
619
656
|
const res = await fetch(imageUrl);
|
|
620
|
-
if (!res.ok) throw new Error(
|
|
657
|
+
if (!res.ok) throw new Error(`Image download failed: ${res.status}`);
|
|
621
658
|
imageBuffer = Buffer.from(await res.arrayBuffer());
|
|
622
659
|
} else {
|
|
623
|
-
throw new Error('imageUrl
|
|
660
|
+
throw new Error('Either imageUrl or imagePath is required.');
|
|
624
661
|
}
|
|
625
662
|
|
|
626
663
|
const uploadId = await uploadPhoto(imageBuffer);
|
|
@@ -666,6 +703,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
666
703
|
uploadPhoto,
|
|
667
704
|
configurePost,
|
|
668
705
|
publishPost,
|
|
706
|
+
sendDm,
|
|
669
707
|
deletePost,
|
|
670
708
|
resolveChallenge,
|
|
671
709
|
resetState,
|
|
@@ -676,7 +714,7 @@ const createInstaApiClient = ({ sessionPath }) => {
|
|
|
676
714
|
status[type] = {
|
|
677
715
|
hourly: `${c.hourly}/${HOURLY_LIMIT[type]}`,
|
|
678
716
|
daily: `${c.daily}/${DAILY_LIMIT[type]}`,
|
|
679
|
-
delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}
|
|
717
|
+
delay: `${DELAY[type]?.[0]}~${DELAY[type]?.[1]}s`,
|
|
680
718
|
};
|
|
681
719
|
}
|
|
682
720
|
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('; ');
|
|
@@ -100,7 +100,7 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
100
100
|
const loginData = await loginRes.json();
|
|
101
101
|
|
|
102
102
|
if (loginData.checkpoint_url || loginData.message === 'challenge_required') {
|
|
103
|
-
//
|
|
103
|
+
// Attempt automatic challenge resolution (choice=0 = "This was me")
|
|
104
104
|
const challengeRes = await fetch('https://www.instagram.com/api/v1/challenge/web/action/', {
|
|
105
105
|
method: 'POST',
|
|
106
106
|
headers: {
|
|
@@ -120,11 +120,11 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
120
120
|
const challengeData = await challengeRes.json().catch(() => ({}));
|
|
121
121
|
if (challengeData.status !== 'ok') {
|
|
122
122
|
throw new Error(
|
|
123
|
-
'challenge_required:
|
|
123
|
+
'challenge_required: Automatic resolution failed. Please complete identity verification in the browser. ' +
|
|
124
124
|
(loginData.checkpoint_url || ''),
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
|
-
//
|
|
127
|
+
// Re-login after challenge resolution
|
|
128
128
|
const retryRes = await fetch('https://www.instagram.com/api/v1/web/accounts/login/ajax/', {
|
|
129
129
|
method: 'POST',
|
|
130
130
|
headers: {
|
|
@@ -158,16 +158,16 @@ const createAskForAuthentication = ({ sessionPath }) => async ({
|
|
|
158
158
|
|
|
159
159
|
if (loginData.two_factor_required) {
|
|
160
160
|
throw new Error(
|
|
161
|
-
'
|
|
161
|
+
'Two-factor authentication (2FA) is required. Please complete verification in the browser first.',
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
if (!loginData.authenticated) {
|
|
166
166
|
const reason = loginData.message || loginData.status || 'unknown';
|
|
167
|
-
throw new Error(
|
|
167
|
+
throw new Error(`Instagram login failed: ${reason}`);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
//
|
|
170
|
+
// Save session
|
|
171
171
|
saveInstaSession(sessionPath, cookies);
|
|
172
172
|
|
|
173
173
|
return {
|