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
|
@@ -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 {
|
|
@@ -239,11 +239,209 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
239
239
|
});
|
|
240
240
|
},
|
|
241
241
|
|
|
242
|
+
async sendDm({ username, threadId, text } = {}) {
|
|
243
|
+
const target = String(username || '').trim();
|
|
244
|
+
const tid = String(threadId || '').trim();
|
|
245
|
+
const msg = String(text || '').trim();
|
|
246
|
+
if (!target && !tid) throw new Error('username or threadId is required.');
|
|
247
|
+
if (!msg) throw new Error('text is required.');
|
|
248
|
+
|
|
249
|
+
const { chromium } = require('playwright');
|
|
250
|
+
const path = require('path');
|
|
251
|
+
const userDataDir = path.join(path.dirname(sessionPath), '..', 'browser-data', 'insta');
|
|
252
|
+
const fs = require('fs');
|
|
253
|
+
if (!fs.existsSync(userDataDir)) fs.mkdirSync(userDataDir, { recursive: true });
|
|
254
|
+
|
|
255
|
+
// Determine DM URL
|
|
256
|
+
let dmUrl;
|
|
257
|
+
if (tid) {
|
|
258
|
+
dmUrl = `https://www.instagram.com/direct/t/${tid}/`;
|
|
259
|
+
} else {
|
|
260
|
+
dmUrl = `https://www.instagram.com/direct/new/`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
264
|
+
headless: true,
|
|
265
|
+
viewport: { width: 1280, height: 800 },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const page = context.pages()[0] || await context.newPage();
|
|
270
|
+
|
|
271
|
+
if (!tid && target) {
|
|
272
|
+
// New DM: go to new message, search for user
|
|
273
|
+
await page.goto('https://www.instagram.com/direct/new/', { waitUntil: 'domcontentloaded' });
|
|
274
|
+
await page.waitForTimeout(3000);
|
|
275
|
+
|
|
276
|
+
// Search for recipient
|
|
277
|
+
const searchInput = page.locator('input[name="queryBox"]').or(page.getByPlaceholder(/검색|Search/i));
|
|
278
|
+
await searchInput.first().waitFor({ timeout: 10000 });
|
|
279
|
+
await searchInput.first().fill(target);
|
|
280
|
+
await page.waitForTimeout(2000);
|
|
281
|
+
|
|
282
|
+
// Click the user result
|
|
283
|
+
const userResult = page.locator(`text=${target}`).first();
|
|
284
|
+
await userResult.click();
|
|
285
|
+
await page.waitForTimeout(1000);
|
|
286
|
+
|
|
287
|
+
// Click chat/next button
|
|
288
|
+
const chatBtn = page.getByRole('button', { name: /채팅|Chat|다음|Next/i });
|
|
289
|
+
await chatBtn.first().click();
|
|
290
|
+
await page.waitForTimeout(2000);
|
|
291
|
+
} else {
|
|
292
|
+
await page.goto(dmUrl, { waitUntil: 'domcontentloaded' });
|
|
293
|
+
await page.waitForTimeout(3000);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Dismiss popups
|
|
297
|
+
try {
|
|
298
|
+
const btn = page.getByRole('button', { name: /나중에|Not Now/i });
|
|
299
|
+
if (await btn.first().isVisible({ timeout: 2000 })) await btn.first().click();
|
|
300
|
+
} catch {}
|
|
301
|
+
|
|
302
|
+
// Send message
|
|
303
|
+
const input = page.locator('[role="textbox"]').first();
|
|
304
|
+
await input.waitFor({ timeout: 10000 });
|
|
305
|
+
await input.click();
|
|
306
|
+
await page.keyboard.type(msg);
|
|
307
|
+
await page.waitForTimeout(500);
|
|
308
|
+
await page.keyboard.press('Enter');
|
|
309
|
+
await page.waitForTimeout(3000);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
provider: 'insta',
|
|
313
|
+
mode: 'dm',
|
|
314
|
+
to: target || tid,
|
|
315
|
+
text: msg,
|
|
316
|
+
status: 'ok',
|
|
317
|
+
};
|
|
318
|
+
} finally {
|
|
319
|
+
await context.close();
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
async listMessages({ threadId } = {}) {
|
|
324
|
+
const tid = String(threadId || '').trim();
|
|
325
|
+
if (!tid) throw new Error('threadId is required.');
|
|
326
|
+
|
|
327
|
+
const { chromium } = require('playwright');
|
|
328
|
+
const path = require('path');
|
|
329
|
+
const fs = require('fs');
|
|
330
|
+
const userDataDir = path.join(path.dirname(sessionPath), '..', 'browser-data', 'insta');
|
|
331
|
+
if (!fs.existsSync(userDataDir)) fs.mkdirSync(userDataDir, { recursive: true });
|
|
332
|
+
|
|
333
|
+
const context = await chromium.launchPersistentContext(userDataDir, {
|
|
334
|
+
headless: true,
|
|
335
|
+
viewport: { width: 1280, height: 800 },
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const page = context.pages()[0] || await context.newPage();
|
|
340
|
+
await page.goto(`https://www.instagram.com/direct/t/${tid}/`, { waitUntil: 'domcontentloaded' });
|
|
341
|
+
await page.waitForTimeout(5000);
|
|
342
|
+
|
|
343
|
+
// Dismiss popups
|
|
344
|
+
try {
|
|
345
|
+
const btn = page.getByRole('button', { name: /나중에|Not Now/i });
|
|
346
|
+
if (await btn.first().isVisible({ timeout: 2000 })) await btn.first().click();
|
|
347
|
+
} catch {}
|
|
348
|
+
await page.waitForTimeout(1000);
|
|
349
|
+
|
|
350
|
+
// Extract messages from DOM
|
|
351
|
+
const messages = await page.evaluate(() => {
|
|
352
|
+
const result = [];
|
|
353
|
+
// Find message containers - Instagram uses div with role="row" or specific data attributes
|
|
354
|
+
const rows = document.querySelectorAll('div[role="row"]');
|
|
355
|
+
rows.forEach((row) => {
|
|
356
|
+
const textEl = row.querySelector('div[dir="auto"]');
|
|
357
|
+
if (!textEl) return;
|
|
358
|
+
const text = textEl.innerText?.trim();
|
|
359
|
+
if (!text) return;
|
|
360
|
+
|
|
361
|
+
// Determine if sent or received by checking position/style
|
|
362
|
+
const wrapper = row.closest('[class]');
|
|
363
|
+
const style = wrapper ? window.getComputedStyle(wrapper) : null;
|
|
364
|
+
const isSent = row.innerHTML.includes('rgb(99, 91, 255)') ||
|
|
365
|
+
row.innerHTML.includes('#635BFF') ||
|
|
366
|
+
row.querySelector('[style*="flex-end"]') !== null;
|
|
367
|
+
|
|
368
|
+
result.push({ text, isSent });
|
|
369
|
+
});
|
|
370
|
+
return result;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// If role="row" didn't work, try alternative extraction
|
|
374
|
+
if (messages.length === 0) {
|
|
375
|
+
const altMessages = await page.evaluate(() => {
|
|
376
|
+
const result = [];
|
|
377
|
+
const allDivs = document.querySelectorAll('div[dir="auto"]');
|
|
378
|
+
const seen = new Set();
|
|
379
|
+
allDivs.forEach((el) => {
|
|
380
|
+
const text = el.innerText?.trim();
|
|
381
|
+
if (!text || text.length > 500 || seen.has(text)) return;
|
|
382
|
+
// Skip UI elements
|
|
383
|
+
if (['메시지 입력...', '검색', 'Message...'].includes(text)) return;
|
|
384
|
+
if (el.closest('nav') || el.closest('header')) return;
|
|
385
|
+
seen.add(text);
|
|
386
|
+
|
|
387
|
+
// Check if element is in right-aligned (sent) bubble
|
|
388
|
+
const rect = el.getBoundingClientRect();
|
|
389
|
+
const isSent = rect.left > window.innerWidth / 2;
|
|
390
|
+
|
|
391
|
+
result.push({ text, isSent });
|
|
392
|
+
});
|
|
393
|
+
return result;
|
|
394
|
+
});
|
|
395
|
+
if (altMessages.length > 0) messages.push(...altMessages);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Get thread participant name
|
|
399
|
+
const participant = await page.evaluate(() => {
|
|
400
|
+
const header = document.querySelector('header');
|
|
401
|
+
if (!header) return null;
|
|
402
|
+
const spans = header.querySelectorAll('span');
|
|
403
|
+
for (const s of spans) {
|
|
404
|
+
const t = s.innerText?.trim();
|
|
405
|
+
if (t && !['메시지', 'Direct', '뒤로'].includes(t) && t.length < 30) return t;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
provider: 'insta',
|
|
412
|
+
mode: 'messages',
|
|
413
|
+
threadId: tid,
|
|
414
|
+
participant,
|
|
415
|
+
totalCount: messages.length,
|
|
416
|
+
messages,
|
|
417
|
+
};
|
|
418
|
+
} finally {
|
|
419
|
+
await context.close();
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
async listComments({ postId } = {}) {
|
|
424
|
+
return withProviderSession(async () => {
|
|
425
|
+
const shortcode = String(postId || '').trim();
|
|
426
|
+
if (!shortcode) {
|
|
427
|
+
throw new Error('postId (shortcode) is required.');
|
|
428
|
+
}
|
|
429
|
+
const comments = await instaApi.getComments(shortcode);
|
|
430
|
+
return {
|
|
431
|
+
provider: 'insta',
|
|
432
|
+
mode: 'comments',
|
|
433
|
+
postId: shortcode,
|
|
434
|
+
totalCount: comments.length,
|
|
435
|
+
comments,
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
},
|
|
439
|
+
|
|
242
440
|
async analyzePost({ postId } = {}) {
|
|
243
441
|
return withProviderSession(async () => {
|
|
244
442
|
const shortcode = String(postId || '').trim();
|
|
245
443
|
if (!shortcode) {
|
|
246
|
-
throw new Error('postId(shortcode)
|
|
444
|
+
throw new Error('postId (shortcode) is required.');
|
|
247
445
|
}
|
|
248
446
|
const analysis = await smart.analyzePost({ shortcode });
|
|
249
447
|
return {
|
|
@@ -258,7 +456,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
258
456
|
return withProviderSession(async () => {
|
|
259
457
|
const shortcode = String(postId || '').trim();
|
|
260
458
|
if (!shortcode) {
|
|
261
|
-
throw new Error('postId(shortcode)
|
|
459
|
+
throw new Error('postId (shortcode) is required.');
|
|
262
460
|
}
|
|
263
461
|
const mediaId = await instaApi.getMediaIdFromShortcode(shortcode);
|
|
264
462
|
const result = await instaApi.deletePost(mediaId);
|
|
@@ -277,7 +475,7 @@ const createInstaProvider = ({ sessionPath }) => {
|
|
|
277
475
|
provider: 'insta',
|
|
278
476
|
mode: 'resolveChallenge',
|
|
279
477
|
resolved,
|
|
280
|
-
message: resolved ? 'Challenge
|
|
478
|
+
message: resolved ? 'Challenge resolved successfully.' : 'Challenge resolution failed. Please handle it manually in the browser.',
|
|
281
479
|
};
|
|
282
480
|
},
|
|
283
481
|
|
|
@@ -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
|
|
|
@@ -30,28 +30,28 @@ const checkLoginResult = async (page) => {
|
|
|
30
30
|
const patterns = NAVER_LOGIN_ERROR_PATTERNS;
|
|
31
31
|
|
|
32
32
|
if (content.includes(patterns.wrongPassword)) {
|
|
33
|
-
return { success: false, error: 'wrong_password', message: '
|
|
33
|
+
return { success: false, error: 'wrong_password', message: 'Incorrect password.' };
|
|
34
34
|
}
|
|
35
35
|
if (content.includes(patterns.accountProtected)) {
|
|
36
|
-
return { success: false, error: 'account_protected', message: '
|
|
36
|
+
return { success: false, error: 'account_protected', message: 'Account protection is enabled.' };
|
|
37
37
|
}
|
|
38
38
|
if (content.includes(patterns.regionBlocked)) {
|
|
39
|
-
return { success: false, error: 'region_blocked', message: '
|
|
39
|
+
return { success: false, error: 'region_blocked', message: 'Access from a disallowed region was detected.' };
|
|
40
40
|
}
|
|
41
41
|
if (content.includes(patterns.usageRestricted)) {
|
|
42
|
-
return { success: false, error: 'usage_restricted', message: '
|
|
42
|
+
return { success: false, error: 'usage_restricted', message: 'Abnormal activity detected. Usage has been restricted.' };
|
|
43
43
|
}
|
|
44
44
|
if (content.includes(patterns.twoFactor)) {
|
|
45
|
-
return { success: false, error: 'two_factor', message: '
|
|
45
|
+
return { success: false, error: 'two_factor', message: 'Two-factor authentication required. Please log in using --manual mode.' };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
//
|
|
48
|
+
// Captcha detection
|
|
49
49
|
const hasCaptcha = patterns.captcha.some((p) => content.toLowerCase().includes(p.toLowerCase()));
|
|
50
50
|
if (hasCaptcha) {
|
|
51
|
-
return { success: false, error: 'captcha', message: '
|
|
51
|
+
return { success: false, error: 'captcha', message: 'Captcha detected. Please use --manual mode.' };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// Success (including operation violation notice)
|
|
55
55
|
if (content.includes(patterns.operationViolation) || content.includes(patterns.newDevice)) {
|
|
56
56
|
return { success: true };
|
|
57
57
|
}
|
|
@@ -61,7 +61,7 @@ const checkLoginResult = async (page) => {
|
|
|
61
61
|
return { success: true };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
return { success: false, error: 'unknown', message: '
|
|
64
|
+
return { success: false, error: 'unknown', message: 'Unable to verify login status.' };
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
@@ -76,7 +76,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
|
76
76
|
const resolvedPassword = password || readNaverCredentials().password;
|
|
77
77
|
|
|
78
78
|
if (!manual && (!resolvedUsername || !resolvedPassword)) {
|
|
79
|
-
throw new Error('
|
|
79
|
+
throw new Error('Naver login requires id/pw. Set the NAVER_USERNAME/NAVER_PASSWORD environment variables or use --manual mode.');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const browser = await chromium.launch({
|
|
@@ -102,13 +102,13 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
|
102
102
|
if (manual) {
|
|
103
103
|
console.log('');
|
|
104
104
|
console.log('==============================');
|
|
105
|
-
console.log('
|
|
106
|
-
console.log('
|
|
107
|
-
console.log('
|
|
105
|
+
console.log('Switching to manual login mode.');
|
|
106
|
+
console.log('Please complete the Naver login in the browser.');
|
|
107
|
+
console.log('Please complete the login within 5 minutes.');
|
|
108
108
|
console.log('==============================');
|
|
109
109
|
loginSuccess = await waitForNaverLoginFinish(page, context, 300000);
|
|
110
110
|
} else {
|
|
111
|
-
//
|
|
111
|
+
// Inject ID/PW via JS (instead of fill() — bypasses bot detection)
|
|
112
112
|
await page.evaluate((id) => {
|
|
113
113
|
const el = document.getElementById('id');
|
|
114
114
|
if (el) el.value = id;
|
|
@@ -121,14 +121,14 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
|
121
121
|
}, resolvedPassword);
|
|
122
122
|
await sleep(300);
|
|
123
123
|
|
|
124
|
-
//
|
|
124
|
+
// Check "keep me logged in"
|
|
125
125
|
const keepCheck = await page.$(NAVER_LOGIN_SELECTORS.keepLogin);
|
|
126
126
|
if (keepCheck) {
|
|
127
127
|
await keepCheck.click().catch(() => {});
|
|
128
128
|
await sleep(300);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
//
|
|
131
|
+
// Click login button
|
|
132
132
|
const loginBtn = await page.$(NAVER_LOGIN_SELECTORS.submit);
|
|
133
133
|
if (loginBtn) {
|
|
134
134
|
await loginBtn.click();
|
|
@@ -137,7 +137,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
|
137
137
|
}
|
|
138
138
|
await sleep(3000);
|
|
139
139
|
|
|
140
|
-
//
|
|
140
|
+
// Check result
|
|
141
141
|
const result = await checkLoginResult(page);
|
|
142
142
|
if (!result.success) {
|
|
143
143
|
throw new Error(result.message);
|
|
@@ -145,7 +145,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
|
145
145
|
|
|
146
146
|
loginSuccess = await waitForNaverLoginFinish(page, context, 15000);
|
|
147
147
|
if (!loginSuccess) {
|
|
148
|
-
// URL
|
|
148
|
+
// Additional URL-based check
|
|
149
149
|
const url = page.url();
|
|
150
150
|
if (url.includes('naver.com') && !url.includes('nid.naver.com/nidlogin')) {
|
|
151
151
|
loginSuccess = true;
|
|
@@ -154,7 +154,7 @@ const createAskForAuthentication = ({ sessionPath, naverApi }) => async ({
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
if (!loginSuccess) {
|
|
157
|
-
throw new Error('
|
|
157
|
+
throw new Error('Naver login failed. Please verify your id/password or use --manual mode.');
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
await persistNaverSession(context, sessionPath);
|
|
@@ -60,29 +60,29 @@ const createImageComponent = (imgData) => ({
|
|
|
60
60
|
const stripHtmlTags = (html) => html.replace(/<[^>]*>/g, '');
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* HTML
|
|
64
|
-
* Primary:
|
|
65
|
-
* Fallback:
|
|
63
|
+
* Converts HTML to an array of Naver editor components.
|
|
64
|
+
* Primary: Naver API (upconvert.editor.naver.com)
|
|
65
|
+
* Fallback: Custom parsing
|
|
66
66
|
*/
|
|
67
67
|
const convertHtmlToEditorComponents = async (naverApi, html, imageComponents = []) => {
|
|
68
|
-
// 1.
|
|
68
|
+
// 1. Try Naver API conversion
|
|
69
69
|
const apiComponents = await naverApi.convertHtmlToComponents(html);
|
|
70
70
|
if (Array.isArray(apiComponents) && apiComponents.length > 0) {
|
|
71
|
-
//
|
|
71
|
+
// Place images at the top of the post (Tistory style)
|
|
72
72
|
return [...imageComponents, ...apiComponents];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// 2. Fallback:
|
|
75
|
+
// 2. Fallback: Custom parsing (images placed at the top)
|
|
76
76
|
const textComponents = parseHtmlToComponents(html, []);
|
|
77
77
|
return [...imageComponents, ...textComponents];
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* HTML
|
|
82
|
-
* Python
|
|
81
|
+
* Manually parses HTML and converts it to Naver editor components.
|
|
82
|
+
* Ported from Python's process_html_to_components()
|
|
83
83
|
*/
|
|
84
84
|
const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
85
|
-
// heading(h1-h6)
|
|
85
|
+
// Split by heading (h1-h6) or strong tags
|
|
86
86
|
const segments = html.split(/(<h[1-6][^>]*>.*?<\/h[1-6]>|<strong>.*?<\/strong>)/is);
|
|
87
87
|
const components = [];
|
|
88
88
|
const images = [...imageComponents];
|
|
@@ -96,7 +96,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
|
96
96
|
const isStrong = /<strong>/i.test(trimmed);
|
|
97
97
|
const isBoldSection = isHeading || isStrong;
|
|
98
98
|
|
|
99
|
-
// heading
|
|
99
|
+
// Skip heading tags themselves (same as Python code's continue)
|
|
100
100
|
if (/^<h[1-6][^>]*>.*<\/h[1-6]>$/is.test(trimmed)) {
|
|
101
101
|
const text = stripHtmlTags(trimmed);
|
|
102
102
|
if (!text.trim()) continue;
|
|
@@ -111,7 +111,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
|
111
111
|
ctype: 'text',
|
|
112
112
|
}));
|
|
113
113
|
} else {
|
|
114
|
-
//
|
|
114
|
+
// Insert image
|
|
115
115
|
if (images.length > 0) {
|
|
116
116
|
components.push(images.shift());
|
|
117
117
|
}
|
|
@@ -125,7 +125,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
|
125
125
|
continue;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
//
|
|
128
|
+
// Plain text segment
|
|
129
129
|
const text = stripHtmlTags(trimmed);
|
|
130
130
|
if (!text.trim()) continue;
|
|
131
131
|
|
|
@@ -146,7 +146,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
|
146
146
|
ctype: 'quotation',
|
|
147
147
|
}));
|
|
148
148
|
} else {
|
|
149
|
-
//
|
|
149
|
+
// Regular paragraphs: split by <p> or <br>
|
|
150
150
|
const paragraphs = text.split(/\n+/).filter((p) => p.trim());
|
|
151
151
|
for (const para of paragraphs) {
|
|
152
152
|
components.push(createTextComponent(para.trim()));
|
|
@@ -154,7 +154,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
//
|
|
157
|
+
// Append remaining images
|
|
158
158
|
for (const img of images) {
|
|
159
159
|
components.push(img);
|
|
160
160
|
}
|
|
@@ -163,7 +163,7 @@ const parseHtmlToComponents = (html, imageComponents = []) => {
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
166
|
+
* Intersperses images between API-returned components.
|
|
167
167
|
*/
|
|
168
168
|
const intersperse = (components, imageComponents) => {
|
|
169
169
|
if (!imageComponents.length) return components;
|
|
@@ -181,7 +181,7 @@ const intersperse = (components, imageComponents) => {
|
|
|
181
181
|
result.push(comp);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
//
|
|
184
|
+
// Append remaining images
|
|
185
185
|
for (const img of images) {
|
|
186
186
|
result.push(img);
|
|
187
187
|
}
|