viruagent-cli 0.7.2 → 0.8.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/README.ko.md +17 -0
- package/README.md +18 -0
- package/bin/index.js +68 -2
- package/package.json +10 -3
- package/skills/va-naver/SKILL.md +6 -2
- package/skills/va-naver-cafe-id/SKILL.md +50 -0
- package/skills/va-naver-cafe-join/SKILL.md +56 -0
- package/skills/va-naver-cafe-list/SKILL.md +41 -0
- package/skills/va-naver-cafe-write/SKILL.md +85 -0
- package/skills/va-shared/SKILL.md +14 -0
- package/src/providers/naver/cafeApiClient.js +599 -0
- package/src/providers/naver/index.js +261 -0
- package/src/runner.js +59 -1
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const CAFE_MOBILE_BASE = 'https://apis.naver.com/cafe-web/cafe-mobile';
|
|
6
|
+
const CAFE_EDITOR_BASE = 'https://apis.cafe.naver.com/editor';
|
|
7
|
+
const MOBILE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
|
|
8
|
+
const PC_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
9
|
+
|
|
10
|
+
const createError = (code, message, hint) => {
|
|
11
|
+
const err = new Error(message);
|
|
12
|
+
err.code = code;
|
|
13
|
+
if (hint) err.hint = hint;
|
|
14
|
+
return err;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const readSessionCookies = (sessionPath) => {
|
|
18
|
+
const resolvedPath = path.resolve(sessionPath);
|
|
19
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
20
|
+
throw createError('SESSION_NOT_FOUND', `Session file not found: ${resolvedPath}`, 'viruagent-cli login --provider naver');
|
|
21
|
+
}
|
|
22
|
+
let raw;
|
|
23
|
+
try {
|
|
24
|
+
raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
throw createError('SESSION_PARSE_ERROR', `Failed to parse session: ${e.message}`);
|
|
27
|
+
}
|
|
28
|
+
const cookies = Array.isArray(raw?.cookies) ? raw.cookies : [];
|
|
29
|
+
const naverCookies = cookies
|
|
30
|
+
.filter((c) => c && c.name && c.value !== undefined && c.value !== null)
|
|
31
|
+
.filter((c) => {
|
|
32
|
+
if (!c.domain) return true;
|
|
33
|
+
return String(c.domain).includes('naver.com');
|
|
34
|
+
})
|
|
35
|
+
.map((c) => `${c.name}=${c.value}`);
|
|
36
|
+
if (!naverCookies.length) {
|
|
37
|
+
throw createError('NO_COOKIES', 'No valid naver cookies found', 'viruagent-cli login --provider naver');
|
|
38
|
+
}
|
|
39
|
+
return naverCookies.join('; ');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const createCafeApiClient = ({ sessionPath }) => {
|
|
43
|
+
const getCookieStr = () => readSessionCookies(sessionPath);
|
|
44
|
+
|
|
45
|
+
const mobileHeaders = (cookieStr) => ({
|
|
46
|
+
Cookie: cookieStr,
|
|
47
|
+
'User-Agent': MOBILE_UA,
|
|
48
|
+
Referer: 'https://m.cafe.naver.com/',
|
|
49
|
+
Accept: 'application/json, text/plain, */*',
|
|
50
|
+
'x-cafe-product': 'mweb',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const pcHeaders = (cookieStr) => ({
|
|
54
|
+
Cookie: cookieStr,
|
|
55
|
+
'User-Agent': PC_UA,
|
|
56
|
+
Referer: 'https://cafe.naver.com/',
|
|
57
|
+
Accept: 'application/json, text/plain, */*',
|
|
58
|
+
'X-Cafe-Product': 'pc',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const apiGet = async (url, headers) => {
|
|
62
|
+
const res = await fetch(url, {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
headers,
|
|
65
|
+
redirect: 'follow',
|
|
66
|
+
});
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
try {
|
|
69
|
+
return { status: res.status, data: JSON.parse(text) };
|
|
70
|
+
} catch {
|
|
71
|
+
return { status: res.status, data: null, raw: text };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const apiPost = async (url, body, headers) => {
|
|
76
|
+
const res = await fetch(url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers,
|
|
79
|
+
body,
|
|
80
|
+
redirect: 'follow',
|
|
81
|
+
});
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
try {
|
|
84
|
+
return { status: res.status, data: JSON.parse(text) };
|
|
85
|
+
} catch {
|
|
86
|
+
return { status: res.status, data: null, raw: text };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ── cafeId 추출 ──
|
|
91
|
+
|
|
92
|
+
const extractCafeId = async (cafeUrl) => {
|
|
93
|
+
const cookieStr = getCookieStr();
|
|
94
|
+
// cafeUrl can be full URL or slug
|
|
95
|
+
const slug = cafeUrl.replace(/^https?:\/\/(m\.)?cafe\.naver\.com\/?/, '').replace(/\/$/, '').split('/')[0];
|
|
96
|
+
if (!slug) throw createError('INVALID_CAFE_URL', 'Could not extract cafe slug from URL');
|
|
97
|
+
|
|
98
|
+
// Try mobile URL first
|
|
99
|
+
const mobileUrl = `https://m.cafe.naver.com/ca-fe/${slug}`;
|
|
100
|
+
const res = await fetch(mobileUrl, {
|
|
101
|
+
method: 'GET',
|
|
102
|
+
headers: { Cookie: cookieStr, 'User-Agent': MOBILE_UA },
|
|
103
|
+
redirect: 'follow',
|
|
104
|
+
});
|
|
105
|
+
const html = await res.text();
|
|
106
|
+
|
|
107
|
+
let match =
|
|
108
|
+
html.match(/g_sClubId\s*=\s*["']?(\d+)/) ||
|
|
109
|
+
html.match(/"clubId"\s*:\s*(\d+)/) ||
|
|
110
|
+
html.match(/"cafeId"\s*:\s*(\d+)/) ||
|
|
111
|
+
html.match(/clubid=(\d+)/i) ||
|
|
112
|
+
html.match(/cafes\/(\d+)/);
|
|
113
|
+
if (match) return { cafeId: match[1], slug };
|
|
114
|
+
|
|
115
|
+
// Try desktop URL
|
|
116
|
+
const desktopUrl = `https://cafe.naver.com/${slug}`;
|
|
117
|
+
const res2 = await fetch(desktopUrl, {
|
|
118
|
+
method: 'GET',
|
|
119
|
+
headers: { Cookie: cookieStr, 'User-Agent': PC_UA },
|
|
120
|
+
redirect: 'follow',
|
|
121
|
+
});
|
|
122
|
+
const html2 = await res2.text();
|
|
123
|
+
match =
|
|
124
|
+
html2.match(/g_sClubId\s*=\s*["']?(\d+)/) ||
|
|
125
|
+
html2.match(/"clubId"\s*:\s*(\d+)/) ||
|
|
126
|
+
html2.match(/"cafeId"\s*:\s*(\d+)/) ||
|
|
127
|
+
html2.match(/clubid=(\d+)/i);
|
|
128
|
+
if (match) return { cafeId: match[1], slug };
|
|
129
|
+
|
|
130
|
+
throw createError('CAFE_ID_NOT_FOUND', `Could not extract cafeId from ${cafeUrl}`);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// ── 카페 가입 ──
|
|
134
|
+
|
|
135
|
+
const getJoinForm = async (cafeId) => {
|
|
136
|
+
const cookieStr = getCookieStr();
|
|
137
|
+
const url = `${CAFE_MOBILE_BASE}/CafeApplyView.json?cafeId=${cafeId}`;
|
|
138
|
+
const res = await apiGet(url, mobileHeaders(cookieStr));
|
|
139
|
+
|
|
140
|
+
if (res.data?.message?.status !== '200') {
|
|
141
|
+
const errCode = res.data?.message?.error?.code || '';
|
|
142
|
+
const errMsg = res.data?.message?.error?.msg || '';
|
|
143
|
+
if (errCode === '3001' || errMsg.includes('이미 회원')) {
|
|
144
|
+
throw createError('ALREADY_JOINED', `Already a member of cafe ${cafeId}`);
|
|
145
|
+
}
|
|
146
|
+
throw createError('CAFE_APPLY_VIEW_FAILED', `CafeApplyView failed: ${errCode} ${errMsg}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const result = res.data.message.result;
|
|
150
|
+
return {
|
|
151
|
+
applyType: result.applyType,
|
|
152
|
+
cafeName: result.mobileCafeApplyProfileInfo?.cafeName || '',
|
|
153
|
+
nickname: result.mobileCafeApplyBodyInfo?.nickname || '',
|
|
154
|
+
clubTempId: result.mobileCafeApplyBodyInfo?.clubTempId || '',
|
|
155
|
+
alimCode: result.mobileCafeApplyBodyInfo?.alimCode || '',
|
|
156
|
+
lastsetno: result.mobileCafeApplyBodyInfo?.lastsetno || 0,
|
|
157
|
+
applyQuestions: result.mobileCafeApplyBodyInfo?.applyQuestions || [],
|
|
158
|
+
needCaptcha: result.mobileCafeApplyCaptcha?.needCaptcha || false,
|
|
159
|
+
captchaKey: result.mobileCafeApplyCaptcha?.captchaKey || '',
|
|
160
|
+
captchaImageUrl: result.mobileCafeApplyCaptcha?.captchaImageUrl || '',
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const checkNickname = async (cafeId, nickname) => {
|
|
165
|
+
const cookieStr = getCookieStr();
|
|
166
|
+
const url = `${CAFE_MOBILE_BASE}/CafeMemberNicknameValid.json?cafeId=${cafeId}&nickname=${encodeURIComponent(nickname)}`;
|
|
167
|
+
const res = await apiGet(url, mobileHeaders(cookieStr));
|
|
168
|
+
return res.data?.message?.status === '200';
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const validateCaptcha = async (captchaKey, captchaValue) => {
|
|
172
|
+
const cookieStr = getCookieStr();
|
|
173
|
+
const url = `${CAFE_MOBILE_BASE}/CaptchaValidate.json?captchaKey=${encodeURIComponent(captchaKey)}&captchaValue=${encodeURIComponent(captchaValue)}&captchaType=image`;
|
|
174
|
+
const res = await apiGet(url, mobileHeaders(cookieStr));
|
|
175
|
+
|
|
176
|
+
if (res.data?.message?.status === '200') {
|
|
177
|
+
const result = res.data.message.result || {};
|
|
178
|
+
return {
|
|
179
|
+
valid: result.valid,
|
|
180
|
+
captchaKey: result.captchaKey || null,
|
|
181
|
+
captchaImageUrl: result.captchaImageUrl || null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { valid: false, captchaKey: null, captchaImageUrl: null };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const downloadCaptchaImage = async (captchaImageUrl) => {
|
|
188
|
+
const cookieStr = getCookieStr();
|
|
189
|
+
const res = await fetch(captchaImageUrl, {
|
|
190
|
+
headers: {
|
|
191
|
+
Cookie: cookieStr,
|
|
192
|
+
'User-Agent': MOBILE_UA,
|
|
193
|
+
Referer: 'https://m.cafe.naver.com/',
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) throw createError('CAPTCHA_DOWNLOAD_FAILED', `Failed to download captcha image: ${res.status}`);
|
|
197
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
198
|
+
return buffer.toString('base64');
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const solveCaptchaWith2Captcha = async (imageBase64, apiKey) => {
|
|
202
|
+
const submitRes = await fetch('https://2captcha.com/in.php', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
205
|
+
body: new URLSearchParams({
|
|
206
|
+
key: apiKey,
|
|
207
|
+
method: 'base64',
|
|
208
|
+
body: imageBase64,
|
|
209
|
+
json: '1',
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
const submitData = await submitRes.json();
|
|
213
|
+
if (submitData.status !== 1) {
|
|
214
|
+
throw createError('CAPTCHA_SUBMIT_FAILED', `2Captcha submit failed: ${submitData.request}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const captchaId = submitData.request;
|
|
218
|
+
|
|
219
|
+
// Poll for result (max 120s)
|
|
220
|
+
for (let i = 0; i < 24; i++) {
|
|
221
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
222
|
+
const pollRes = await fetch(
|
|
223
|
+
`https://2captcha.com/res.php?key=${apiKey}&action=get&id=${captchaId}&json=1`,
|
|
224
|
+
);
|
|
225
|
+
const pollData = await pollRes.json();
|
|
226
|
+
if (pollData.status === 1) return pollData.request;
|
|
227
|
+
if (pollData.request !== 'CAPCHA_NOT_READY') {
|
|
228
|
+
throw createError('CAPTCHA_POLL_FAILED', `2Captcha poll failed: ${pollData.request}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
throw createError('CAPTCHA_TIMEOUT', '2Captcha timeout (120s)');
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const submitJoin = async (cafeId, { alimCode, clubTempId, applyPayload }) => {
|
|
235
|
+
const cookieStr = getCookieStr();
|
|
236
|
+
const queryParams = new URLSearchParams({
|
|
237
|
+
cafeId,
|
|
238
|
+
alimCode,
|
|
239
|
+
clubTempId,
|
|
240
|
+
requestFrom: 'B',
|
|
241
|
+
});
|
|
242
|
+
const url = `${CAFE_MOBILE_BASE}/CafeApply.json?${queryParams}`;
|
|
243
|
+
const body = `applyRequestJson=${encodeURIComponent(JSON.stringify(applyPayload))}`;
|
|
244
|
+
const referer = `https://m.cafe.naver.com/ca-fe/web/cafes/${cafeId}/join`;
|
|
245
|
+
|
|
246
|
+
const headers = {
|
|
247
|
+
...mobileHeaders(cookieStr),
|
|
248
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
249
|
+
Referer: referer,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const res = await apiPost(url, body, headers);
|
|
253
|
+
|
|
254
|
+
if (res.data?.message?.status === '200' || (res.raw === '' || res.raw?.trim() === '')) {
|
|
255
|
+
return { success: true, message: 'Success' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const errCode = res.data?.message?.error?.code || '';
|
|
259
|
+
const errMsg = res.data?.message?.error?.msg || '';
|
|
260
|
+
throw createError('CAFE_JOIN_FAILED', `Join failed: ${errCode} ${errMsg}`.trim());
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// ── 카페 게시판 목록 ──
|
|
264
|
+
|
|
265
|
+
const getBoardList = async (cafeId) => {
|
|
266
|
+
const cookieStr = getCookieStr();
|
|
267
|
+
|
|
268
|
+
// Primary: cafe2 SideMenuList API (PC)
|
|
269
|
+
const url = `https://apis.naver.com/cafe-web/cafe2/SideMenuList?cafeId=${cafeId}`;
|
|
270
|
+
const res = await apiGet(url, pcHeaders(cookieStr));
|
|
271
|
+
if (res.data?.message?.status === '200') {
|
|
272
|
+
const menus = res.data.message.result?.menus || [];
|
|
273
|
+
const writable = menus.filter((m) => m.menuType === 'B');
|
|
274
|
+
return writable.map((m) => ({
|
|
275
|
+
boardId: m.menuId,
|
|
276
|
+
name: m.menuName,
|
|
277
|
+
boardType: m.boardType,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Fallback: mobile boardlist API
|
|
282
|
+
const url2 = `https://apis.naver.com/cafe-web/cafe-boardlist-api/v1/cafes/${cafeId}/boardlist`;
|
|
283
|
+
const res2 = await apiGet(url2, mobileHeaders(cookieStr));
|
|
284
|
+
if (res2.data?.message?.status === '200') {
|
|
285
|
+
const boards = res2.data.message.result?.boardList || [];
|
|
286
|
+
return boards.map((b) => ({
|
|
287
|
+
boardId: b.menuId,
|
|
288
|
+
name: b.menuName,
|
|
289
|
+
boardType: b.boardType,
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
throw createError('BOARD_LIST_FAILED', `Could not fetch board list for cafe ${cafeId}`);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// ── 카페 이미지 업로드 ──
|
|
297
|
+
|
|
298
|
+
const PHOTO_SESSION_URL = 'https://apis.naver.com/cafe-web/cafe-mobile/PhotoInfraSessionKey.json';
|
|
299
|
+
const PHOTO_UPLOAD_DOMAIN = 'cafe.upphoto.naver.com';
|
|
300
|
+
|
|
301
|
+
const getPhotoSessionKey = async () => {
|
|
302
|
+
const cookieStr = getCookieStr();
|
|
303
|
+
const res = await apiPost(PHOTO_SESSION_URL, '', pcHeaders(cookieStr));
|
|
304
|
+
const key = res.data?.message?.result;
|
|
305
|
+
if (!key) throw createError('PHOTO_SESSION_FAILED', 'Failed to get photo session key');
|
|
306
|
+
return key;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const uploadImage = async (sessionKey, imageBuffer, fileName, userId = '') => {
|
|
310
|
+
const boundary = `----FormBoundary${crypto.randomUUID().replace(/-/g, '')}`;
|
|
311
|
+
const body = Buffer.concat([
|
|
312
|
+
Buffer.from(
|
|
313
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="image"; filename="${fileName}"\r\nContent-Type: image/jpeg\r\n\r\n`,
|
|
314
|
+
),
|
|
315
|
+
imageBuffer,
|
|
316
|
+
Buffer.from(`\r\n--${boundary}--\r\n`),
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
const uploadUrl =
|
|
320
|
+
`https://${PHOTO_UPLOAD_DOMAIN}/${sessionKey}/simpleUpload/0` +
|
|
321
|
+
`?userId=${userId}&extractExif=true&extractAnimatedCnt=true&autorotate=true` +
|
|
322
|
+
`&extractDominantColor=false&denyAnimatedImage=false&skipXcamFiltering=false`;
|
|
323
|
+
|
|
324
|
+
const cookieStr = getCookieStr();
|
|
325
|
+
const res = await fetch(uploadUrl, {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: {
|
|
328
|
+
Cookie: cookieStr,
|
|
329
|
+
'User-Agent': PC_UA,
|
|
330
|
+
Referer: 'https://cafe.naver.com/',
|
|
331
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
332
|
+
},
|
|
333
|
+
body,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const text = await res.text();
|
|
337
|
+
const result = {};
|
|
338
|
+
|
|
339
|
+
// Parse pipe-delimited response: url=...|width=800|height=600|...
|
|
340
|
+
if (text.includes('|')) {
|
|
341
|
+
for (const pair of text.split('|')) {
|
|
342
|
+
const idx = pair.indexOf('=');
|
|
343
|
+
if (idx > 0) result[pair.slice(0, idx)] = pair.slice(idx + 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!result.url) throw createError('IMAGE_UPLOAD_FAILED', `Image upload failed: ${text.slice(0, 200)}`);
|
|
348
|
+
|
|
349
|
+
for (const k of ['width', 'height', 'fileSize']) {
|
|
350
|
+
if (result[k]) result[k] = parseInt(result[k], 10) || 0;
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const createImageComponent = (imgData, link) => {
|
|
356
|
+
const seId = () => `SE-${crypto.randomUUID()}`;
|
|
357
|
+
const url = imgData.url || '';
|
|
358
|
+
let domain = 'https://cafeptthumb-phinf.pstatic.net';
|
|
359
|
+
let imgPath = url;
|
|
360
|
+
if (url.startsWith('http')) {
|
|
361
|
+
const u = new URL(url);
|
|
362
|
+
domain = `${u.protocol}//${u.host}`;
|
|
363
|
+
imgPath = u.pathname;
|
|
364
|
+
}
|
|
365
|
+
const comp = {
|
|
366
|
+
id: seId(),
|
|
367
|
+
layout: 'default',
|
|
368
|
+
align: 'center',
|
|
369
|
+
src: `${domain}${imgPath}?type=w1`,
|
|
370
|
+
internalResource: true,
|
|
371
|
+
represent: imgData.represent || false,
|
|
372
|
+
path: imgPath,
|
|
373
|
+
domain,
|
|
374
|
+
fileSize: imgData.fileSize || 0,
|
|
375
|
+
width: imgData.width || 800,
|
|
376
|
+
widthPercentage: 0,
|
|
377
|
+
height: imgData.height || 600,
|
|
378
|
+
originalWidth: imgData.width || 800,
|
|
379
|
+
originalHeight: imgData.height || 600,
|
|
380
|
+
fileName: imgData.fileName || 'image.jpg',
|
|
381
|
+
caption: null,
|
|
382
|
+
format: 'normal',
|
|
383
|
+
displayFormat: 'normal',
|
|
384
|
+
imageLoaded: true,
|
|
385
|
+
contentMode: 'normal',
|
|
386
|
+
origin: { srcFrom: 'local', '@ctype': 'imageOrigin' },
|
|
387
|
+
ai: false,
|
|
388
|
+
'@ctype': 'image',
|
|
389
|
+
};
|
|
390
|
+
if (link) comp.link = link;
|
|
391
|
+
return comp;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const createImageGroup = (imagesData, layout = 'slide') => {
|
|
395
|
+
const _id = () => `SE-${crypto.randomUUID()}`;
|
|
396
|
+
const isCollage = layout === 'collage';
|
|
397
|
+
const numImages = imagesData.length;
|
|
398
|
+
|
|
399
|
+
const images = imagesData.map((imgData, idx) => {
|
|
400
|
+
const url = imgData.url || '';
|
|
401
|
+
let domain = 'https://cafeptthumb-phinf.pstatic.net';
|
|
402
|
+
let imgPath = url;
|
|
403
|
+
if (url.startsWith('http')) {
|
|
404
|
+
const u = new URL(url);
|
|
405
|
+
domain = `${u.protocol}//${u.host}`;
|
|
406
|
+
imgPath = u.pathname;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const typeSuffix = isCollage ? '?type=w1600' : '?type=w1';
|
|
410
|
+
const imgWidth = isCollage ? (imgData.width || 800) : 693;
|
|
411
|
+
const contentMode = isCollage ? 'extend' : 'fit';
|
|
412
|
+
|
|
413
|
+
let widthPct = 0;
|
|
414
|
+
if (isCollage) {
|
|
415
|
+
if (numImages === 1) widthPct = 100;
|
|
416
|
+
else if (idx < numImages - (numImages % 2)) widthPct = 50;
|
|
417
|
+
else widthPct = 100;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
id: _id(),
|
|
422
|
+
layout: 'default',
|
|
423
|
+
src: `${domain}${imgPath}${typeSuffix}`,
|
|
424
|
+
internalResource: true,
|
|
425
|
+
represent: idx === 0,
|
|
426
|
+
path: imgPath,
|
|
427
|
+
domain,
|
|
428
|
+
fileSize: imgData.fileSize || 0,
|
|
429
|
+
width: imgWidth,
|
|
430
|
+
widthPercentage: widthPct,
|
|
431
|
+
height: imgData.height || 600,
|
|
432
|
+
originalWidth: imgData.width || 800,
|
|
433
|
+
originalHeight: imgData.height || 600,
|
|
434
|
+
fileName: imgData.fileName || 'image.jpg',
|
|
435
|
+
caption: null,
|
|
436
|
+
format: 'normal',
|
|
437
|
+
displayFormat: 'normal',
|
|
438
|
+
contentMode,
|
|
439
|
+
origin: { srcFrom: 'local', '@ctype': 'imageOrigin' },
|
|
440
|
+
ai: false,
|
|
441
|
+
'@ctype': 'image',
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
id: _id(),
|
|
447
|
+
layout,
|
|
448
|
+
contentMode: 'extend',
|
|
449
|
+
caption: null,
|
|
450
|
+
images,
|
|
451
|
+
'@ctype': 'imageGroup',
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// ── 카페 글쓰기 ──
|
|
456
|
+
|
|
457
|
+
const seId = () => `SE-${crypto.randomUUID()}`;
|
|
458
|
+
|
|
459
|
+
const getEditorInfo = async (cafeId, menuId) => {
|
|
460
|
+
const cookieStr = getCookieStr();
|
|
461
|
+
const url = `${CAFE_EDITOR_BASE}/v2/cafes/${cafeId}/editor?menuId=${menuId}&from=pc`;
|
|
462
|
+
const res = await apiGet(url, pcHeaders(cookieStr));
|
|
463
|
+
const data = res.data?.result || res.data || {};
|
|
464
|
+
if (!data.token) {
|
|
465
|
+
throw createError('EDITOR_INIT_FAILED', `Editor init failed for cafe ${cafeId}, menu ${menuId}`);
|
|
466
|
+
}
|
|
467
|
+
return data;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const htmlToComponents = async (htmlContent) => {
|
|
471
|
+
// Use Naver upconvert API
|
|
472
|
+
const cookieStr = getCookieStr();
|
|
473
|
+
const wrapped = `<html>\n<body>\n<!--StartFragment-->\n${htmlContent}\n<!--EndFragment-->\n</body>\n</html>`;
|
|
474
|
+
const res = await fetch(
|
|
475
|
+
'https://upconvert.editor.naver.com/blog/html/components?documentWidth=800',
|
|
476
|
+
{
|
|
477
|
+
method: 'POST',
|
|
478
|
+
headers: {
|
|
479
|
+
Cookie: cookieStr,
|
|
480
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
481
|
+
'User-Agent': PC_UA,
|
|
482
|
+
},
|
|
483
|
+
body: Buffer.from(wrapped, 'utf-8'),
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
if (res.ok) {
|
|
488
|
+
const result = await res.json();
|
|
489
|
+
if (Array.isArray(result) && result.length > 0) return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Fallback: simple text component
|
|
493
|
+
const cleanText = htmlContent.replace(/<[^>]*>/g, '').trim();
|
|
494
|
+
if (!cleanText) return [];
|
|
495
|
+
return [{
|
|
496
|
+
id: seId(),
|
|
497
|
+
layout: 'default',
|
|
498
|
+
value: [{
|
|
499
|
+
id: seId(),
|
|
500
|
+
nodes: [{
|
|
501
|
+
id: seId(),
|
|
502
|
+
value: cleanText,
|
|
503
|
+
style: { fontColor: '#333333', fontSizeCode: 'fs16', bold: 'false', '@ctype': 'nodeStyle' },
|
|
504
|
+
'@ctype': 'textNode',
|
|
505
|
+
}],
|
|
506
|
+
style: { align: 'left', lineHeight: '1.8', '@ctype': 'paragraphStyle' },
|
|
507
|
+
'@ctype': 'paragraph',
|
|
508
|
+
}],
|
|
509
|
+
'@ctype': 'text',
|
|
510
|
+
}];
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const buildContentJson = (components) => {
|
|
514
|
+
return JSON.stringify({
|
|
515
|
+
document: {
|
|
516
|
+
version: '2.9.0',
|
|
517
|
+
theme: 'default',
|
|
518
|
+
language: 'ko-KR',
|
|
519
|
+
id: seId(),
|
|
520
|
+
components,
|
|
521
|
+
},
|
|
522
|
+
documentId: '',
|
|
523
|
+
});
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const postArticle = async (cafeId, menuId, title, contentJson, tags, options) => {
|
|
527
|
+
const cookieStr = getCookieStr();
|
|
528
|
+
const url = `${CAFE_EDITOR_BASE}/v2.0/cafes/${cafeId}/menus/${menuId}/articles`;
|
|
529
|
+
const opts = options || {};
|
|
530
|
+
const body = {
|
|
531
|
+
article: {
|
|
532
|
+
cafeId: String(cafeId),
|
|
533
|
+
contentJson,
|
|
534
|
+
from: 'pc',
|
|
535
|
+
menuId: Number(menuId),
|
|
536
|
+
subject: title.trim(),
|
|
537
|
+
tagList: tags || [],
|
|
538
|
+
editorVersion: 4,
|
|
539
|
+
parentId: 0,
|
|
540
|
+
open: opts.open || false,
|
|
541
|
+
naverOpen: opts.naverOpen !== undefined ? opts.naverOpen : true,
|
|
542
|
+
externalOpen: opts.externalOpen !== undefined ? opts.externalOpen : true,
|
|
543
|
+
enableComment: opts.enableComment !== undefined ? opts.enableComment : true,
|
|
544
|
+
enableScrap: opts.enableScrap || false,
|
|
545
|
+
enableCopy: opts.enableCopy || false,
|
|
546
|
+
useAutoSource: opts.useAutoSource !== undefined ? opts.useAutoSource : true,
|
|
547
|
+
cclTypes: opts.cclTypes || [],
|
|
548
|
+
useCcl: false,
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const headers = {
|
|
553
|
+
...pcHeaders(cookieStr),
|
|
554
|
+
'Content-Type': 'application/json',
|
|
555
|
+
Origin: 'https://cafe.naver.com',
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const res = await apiPost(url, JSON.stringify(body), headers);
|
|
559
|
+
|
|
560
|
+
if (res.status === 200) {
|
|
561
|
+
const data = res.data?.result || res.data || {};
|
|
562
|
+
const articleId = data.articleId;
|
|
563
|
+
if (articleId) {
|
|
564
|
+
return {
|
|
565
|
+
articleId,
|
|
566
|
+
articleUrl: `https://cafe.naver.com/ca-fe/cafes/${cafeId}/articles/${articleId}`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
return { articleId: null, articleUrl: null, raw: res.data };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const errInfo = res.data?.error || {};
|
|
573
|
+
throw createError(
|
|
574
|
+
'CAFE_WRITE_FAILED',
|
|
575
|
+
`Cafe post failed: HTTP ${res.status} [${errInfo.errorCode || ''}] ${errInfo.message || ''}`.trim(),
|
|
576
|
+
);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
extractCafeId,
|
|
581
|
+
getJoinForm,
|
|
582
|
+
checkNickname,
|
|
583
|
+
validateCaptcha,
|
|
584
|
+
downloadCaptchaImage,
|
|
585
|
+
solveCaptchaWith2Captcha,
|
|
586
|
+
submitJoin,
|
|
587
|
+
getBoardList,
|
|
588
|
+
getEditorInfo,
|
|
589
|
+
htmlToComponents,
|
|
590
|
+
buildContentJson,
|
|
591
|
+
postArticle,
|
|
592
|
+
getPhotoSessionKey,
|
|
593
|
+
uploadImage,
|
|
594
|
+
createImageComponent,
|
|
595
|
+
createImageGroup,
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
module.exports = createCafeApiClient;
|