viruagent-cli 0.4.2 → 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/package.json +1 -1
- package/src/providers/insta/apiClient.js +54 -54
- package/src/providers/insta/auth.js +10 -10
- package/src/providers/insta/index.js +17 -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 +1 -1
- package/src/services/naverApiClient.js +17 -17
- package/src/services/providerManager.js +1 -1
- package/src/services/tistoryApiClient.js +13 -13
|
@@ -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
|
}
|
|
@@ -4,7 +4,7 @@ const { createImageComponent } = require('./editorConvert');
|
|
|
4
4
|
const { buildKeywordImageCandidates } = require('../tistory/imageSources');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Fetches an image buffer from a URL or local file path.
|
|
8
8
|
*/
|
|
9
9
|
const fetchImageBuffer = async (source) => {
|
|
10
10
|
if (fs.existsSync(source)) {
|
|
@@ -26,7 +26,7 @@ const fetchImageBuffer = async (source) => {
|
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
if (!response.ok) {
|
|
29
|
-
throw new Error(
|
|
29
|
+
throw new Error(`Image download failed: ${response.status} — ${source}`);
|
|
30
30
|
}
|
|
31
31
|
const arrayBuffer = await response.arrayBuffer();
|
|
32
32
|
const urlPath = new URL(response.url || source).pathname;
|
|
@@ -41,7 +41,7 @@ const fetchImageBuffer = async (source) => {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Uploads image sources to Naver and returns an array of editor components.
|
|
45
45
|
*/
|
|
46
46
|
const uploadAndCreateImageComponents = async (naverApi, imageSources, token) => {
|
|
47
47
|
const components = [];
|
|
@@ -65,8 +65,8 @@ const uploadAndCreateImageComponents = async (naverApi, imageSources, token) =>
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* relatedImageKeywords
|
|
69
|
-
*
|
|
68
|
+
* Searches for images from relatedImageKeywords, merges with imageUrls, and uploads them.
|
|
69
|
+
* Reuses buildKeywordImageCandidates from Tistory's imageSources.js.
|
|
70
70
|
*/
|
|
71
71
|
const collectAndUploadImages = async (naverApi, {
|
|
72
72
|
imageUrls = [],
|
|
@@ -76,7 +76,7 @@ const collectAndUploadImages = async (naverApi, {
|
|
|
76
76
|
}) => {
|
|
77
77
|
const collectedUrls = [...imageUrls];
|
|
78
78
|
|
|
79
|
-
//
|
|
79
|
+
// Search image URLs from keywords
|
|
80
80
|
const normalizedKeywords = Array.isArray(relatedImageKeywords)
|
|
81
81
|
? relatedImageKeywords.map((k) => String(k || '').trim()).filter(Boolean)
|
|
82
82
|
: String(relatedImageKeywords || '').split(',').map((k) => k.trim()).filter(Boolean);
|
|
@@ -92,7 +92,7 @@ const collectAndUploadImages = async (naverApi, {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
} catch {
|
|
95
|
-
//
|
|
95
|
+
// Ignore search failures
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -63,7 +63,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
if (!resolved.manual && (!resolved.username || !resolved.password)) {
|
|
66
|
-
throw new Error('
|
|
66
|
+
throw new Error('Naver auto-login requires username/password. Set the NAVER_USERNAME / NAVER_PASSWORD environment variables or use --manual mode.');
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const result = await askForAuthentication(resolved);
|
|
@@ -78,7 +78,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
78
78
|
|
|
79
79
|
async publish(payload) {
|
|
80
80
|
return withProviderSession(async () => {
|
|
81
|
-
const title = payload.title || '
|
|
81
|
+
const title = payload.title || 'Untitled';
|
|
82
82
|
const rawContent = payload.content || '';
|
|
83
83
|
const openType = mapNaverVisibility(payload.visibility);
|
|
84
84
|
const tags = normalizeNaverTagList(payload.tags);
|
|
@@ -91,7 +91,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
91
91
|
const rawCategories = await naverApi.getCategories();
|
|
92
92
|
const categories = Object.entries(rawCategories).map(([name, id]) => ({ name, id: Number(id) })).sort((a, b) => a.id - b.id);
|
|
93
93
|
|
|
94
|
-
//
|
|
94
|
+
// Determine category
|
|
95
95
|
let categoryNo = payload.category;
|
|
96
96
|
if (categoryNo === undefined || categoryNo === null || String(categoryNo).trim() === '') {
|
|
97
97
|
if (categories.length === 0) {
|
|
@@ -107,14 +107,14 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
107
107
|
title,
|
|
108
108
|
openType,
|
|
109
109
|
tags,
|
|
110
|
-
message: '
|
|
110
|
+
message: 'A category is required for publishing. Please check categories and specify a category.',
|
|
111
111
|
categories,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
categoryNo = String(categoryNo);
|
|
116
116
|
|
|
117
|
-
//
|
|
117
|
+
// Image upload (auto-search based on imageUrls + relatedImageKeywords)
|
|
118
118
|
let imageComponents = [];
|
|
119
119
|
const hasImageSources = imageUrls.length > 0 || relatedImageKeywords.length > 0;
|
|
120
120
|
if (autoUploadImages && hasImageSources) {
|
|
@@ -128,7 +128,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
128
128
|
imageComponents = uploadResult.components;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
// HTML
|
|
131
|
+
// Convert HTML to editor components
|
|
132
132
|
const contentComponents = await convertHtmlToEditorComponents(naverApi, rawContent, imageComponents);
|
|
133
133
|
|
|
134
134
|
const result = await naverApi.publishPost({
|
|
@@ -154,7 +154,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
154
154
|
},
|
|
155
155
|
|
|
156
156
|
async saveDraft(payload) {
|
|
157
|
-
//
|
|
157
|
+
// Naver has no draft API, so publish as private (openType: 0)
|
|
158
158
|
return this.publish({
|
|
159
159
|
...payload,
|
|
160
160
|
visibility: 'private',
|
|
@@ -196,7 +196,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
196
196
|
provider: 'naver',
|
|
197
197
|
mode: 'post',
|
|
198
198
|
status: 'invalid_post_id',
|
|
199
|
-
message: 'postId
|
|
199
|
+
message: 'postId is required.',
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -208,7 +208,7 @@ const createNaverProvider = ({ sessionPath }) => {
|
|
|
208
208
|
mode: 'post',
|
|
209
209
|
status: 'not_found',
|
|
210
210
|
postId: resolvedPostId,
|
|
211
|
-
message: '
|
|
211
|
+
message: 'Post not found for the given postId.',
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
return {
|
|
@@ -21,7 +21,7 @@ const persistNaverSession = async (context, targetSessionPath) => {
|
|
|
21
21
|
allCookies.push(...cookies);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
24
|
+
// Deduplicate by name+domain
|
|
25
25
|
const seen = new Set();
|
|
26
26
|
const unique = allCookies.filter((c) => {
|
|
27
27
|
const key = `${c.name}@${c.domain}`;
|
|
@@ -96,7 +96,7 @@ const createNaverWithProviderSession = (askForAuthentication) => async (fn) => {
|
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
if (!loginResult.loggedIn) {
|
|
99
|
-
throw new Error(loginResult.message || '
|
|
99
|
+
throw new Error(loginResult.message || 'Login status could not be verified after session refresh.');
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
return fn();
|
|
@@ -10,19 +10,19 @@ const readNaverCredentials = () => {
|
|
|
10
10
|
const parseNaverSessionError = (error) => {
|
|
11
11
|
const message = String(error?.message || '').toLowerCase();
|
|
12
12
|
return [
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'blogid
|
|
18
|
-
'
|
|
19
|
-
'
|
|
13
|
+
'session file not found',
|
|
14
|
+
'no valid cookies',
|
|
15
|
+
'session expired',
|
|
16
|
+
'login required',
|
|
17
|
+
'find blogid',
|
|
18
|
+
'failed to fetch blog info',
|
|
19
|
+
'log in again',
|
|
20
20
|
'401',
|
|
21
21
|
'403',
|
|
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 normalizeNaverTagList = (value = '') => {
|
|
28
28
|
const source = Array.isArray(value)
|
|
@@ -18,8 +18,8 @@ const {
|
|
|
18
18
|
} = require('./selectors');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* askForAuthentication
|
|
22
|
-
* sessionPath, tistoryApi, pending2faResult
|
|
21
|
+
* askForAuthentication factory.
|
|
22
|
+
* Receives sessionPath, tistoryApi, and pending2faResult via dependency injection.
|
|
23
23
|
*/
|
|
24
24
|
const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult }) => async ({
|
|
25
25
|
headless = false,
|
|
@@ -35,7 +35,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
35
35
|
const shouldAutoFill = !manual;
|
|
36
36
|
|
|
37
37
|
if (!manual && (!resolvedUsername || !resolvedPassword)) {
|
|
38
|
-
throw new Error('
|
|
38
|
+
throw new Error('Tistory login requires id/pw. Please provide id/pw or set TISTORY_USERNAME/TISTORY_PASSWORD environment variables.');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const browser = await chromium.launch({
|
|
@@ -55,7 +55,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
55
55
|
|
|
56
56
|
const kakaoLoginSelector = await pickValue(page, KAKAO_TRIGGER_SELECTORS);
|
|
57
57
|
if (!kakaoLoginSelector) {
|
|
58
|
-
throw new Error('
|
|
58
|
+
throw new Error('Could not find the Kakao login button. Please check if the login page UI has changed.');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
await page.locator(kakaoLoginSelector).click({ timeout: 5000 }).catch(() => {});
|
|
@@ -68,16 +68,16 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
68
68
|
if (manual) {
|
|
69
69
|
console.log('');
|
|
70
70
|
console.log('==============================');
|
|
71
|
-
console.log('
|
|
72
|
-
console.log('
|
|
73
|
-
console.log('
|
|
71
|
+
console.log('Switching to manual login mode.');
|
|
72
|
+
console.log('Please complete ID/PW/2FA verification in the browser.');
|
|
73
|
+
console.log('Login or 2FA must be completed within 5 minutes.');
|
|
74
74
|
console.log('==============================');
|
|
75
75
|
finalLoginStatus = await waitForLoginFinish(page, context, 300000);
|
|
76
76
|
} else if (shouldAutoFill) {
|
|
77
77
|
const usernameFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.username, loginId);
|
|
78
78
|
const passwordFilled = await fillBySelector(page, KAKAO_LOGIN_SELECTORS.password, loginPw);
|
|
79
79
|
if (!usernameFilled || !passwordFilled) {
|
|
80
|
-
throw new Error('
|
|
80
|
+
throw new Error('Could not find Kakao login form input fields. Please check if the Tistory login page has changed.');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
await checkBySelector(page, KAKAO_LOGIN_SELECTORS.rememberLogin);
|
|
@@ -94,7 +94,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
94
94
|
}
|
|
95
95
|
const otpFilled = await fillBySelector(page, LOGIN_OTP_SELECTORS, twoFactorCode);
|
|
96
96
|
if (!otpFilled) {
|
|
97
|
-
throw new Error('OTP
|
|
97
|
+
throw new Error('Could not find the OTP input field. Please check the login page.');
|
|
98
98
|
}
|
|
99
99
|
await page.keyboard.press('Enter');
|
|
100
100
|
finalLoginStatus = await waitForLoginFinish(page, context, 45000);
|
|
@@ -106,7 +106,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
106
106
|
if (hasEmailCodeInput && twoFactorCode) {
|
|
107
107
|
const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
|
|
108
108
|
if (!codeFilled) {
|
|
109
|
-
throw new Error('
|
|
109
|
+
throw new Error('Could not find the 2FA input field. Please check the login page.');
|
|
110
110
|
}
|
|
111
111
|
const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
|
|
112
112
|
if (!confirmed) {
|
|
@@ -122,7 +122,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
122
122
|
|
|
123
123
|
const codeFilled = await fillBySelector(page, KAKAO_2FA_SELECTORS.codeInput, twoFactorCode);
|
|
124
124
|
if (!codeFilled) {
|
|
125
|
-
throw new Error('
|
|
125
|
+
throw new Error('Could not find the Kakao email verification input field. Please check the login page.');
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const confirmed = await clickSubmit(page, KAKAO_2FA_SELECTORS.confirm);
|
|
@@ -140,7 +140,7 @@ const createAskForAuthentication = ({ sessionPath, tistoryApi, pending2faResult
|
|
|
140
140
|
if (pendingTwoFactorAction) {
|
|
141
141
|
return pending2faResult('kakao');
|
|
142
142
|
}
|
|
143
|
-
throw new Error('
|
|
143
|
+
throw new Error('Login failed. Please verify your credentials and ensure TISTORY_USERNAME/TISTORY_PASSWORD environment variables are set correctly.');
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
await context.storageState({ path: sessionPath });
|
|
@@ -2,7 +2,7 @@ const { sleep, imageTrace } = require('./utils');
|
|
|
2
2
|
|
|
3
3
|
const fetchText = async (url, retryCount = 0) => {
|
|
4
4
|
if (!url) {
|
|
5
|
-
throw new Error('
|
|
5
|
+
throw new Error('Text URL is missing.');
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const headers = {
|
|
@@ -22,7 +22,7 @@ const fetchText = async (url, retryCount = 0) => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
if (!response.ok) {
|
|
25
|
-
throw new Error(
|
|
25
|
+
throw new Error(`Text request failed: ${response.status} ${response.statusText}, url=${url}`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
return response.text();
|
|
@@ -31,7 +31,7 @@ const fetchText = async (url, retryCount = 0) => {
|
|
|
31
31
|
await sleep(700);
|
|
32
32
|
return fetchText(url, retryCount + 1);
|
|
33
33
|
}
|
|
34
|
-
throw new Error(
|
|
34
|
+
throw new Error(`Web text download failed: ${error.message}`);
|
|
35
35
|
} finally {
|
|
36
36
|
clearTimeout(timeout);
|
|
37
37
|
}
|
|
@@ -52,7 +52,7 @@ const fetchTextWithHeaders = async (url, headers = {}, retryCount = 0) => {
|
|
|
52
52
|
signal: controller.signal,
|
|
53
53
|
});
|
|
54
54
|
if (!response.ok) {
|
|
55
|
-
throw new Error(
|
|
55
|
+
throw new Error(`Text request failed: ${response.status} ${response.statusText}, url=${url}`);
|
|
56
56
|
}
|
|
57
57
|
return response.text();
|
|
58
58
|
} catch (error) {
|
|
@@ -60,7 +60,7 @@ const fetchTextWithHeaders = async (url, headers = {}, retryCount = 0) => {
|
|
|
60
60
|
await sleep(700);
|
|
61
61
|
return fetchTextWithHeaders(url, headers, retryCount + 1);
|
|
62
62
|
}
|
|
63
|
-
throw new Error(
|
|
63
|
+
throw new Error(`Web text download failed: ${error.message}`);
|
|
64
64
|
} finally {
|
|
65
65
|
clearTimeout(timeout);
|
|
66
66
|
}
|