fix(x-bot): 잘린 긴 리트윗 전체 내용 복원 (hydration)
X long tweet(280자 초과)을 Nitter 타임라인이 간헐적으로 …로 잘라서 주는 경우, 개별 상태 페이지에서 재요청해 전체 내용으로 교체. - parseTweets: …로 끝나는 트윗에 truncated 플래그 부여 - hydrateTruncatedTweets: 잘린 트윗 status 페이지 재요청 후 교체 (best-effort) - fetchTweets/fetchAllTweets에 적용 - fetchSingleTweet을 timeout-safe하게 변경 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
6a24b997dd
commit
2cfc580283
2 changed files with 40 additions and 7 deletions
|
|
@ -176,7 +176,7 @@ async function xBotPlugin(fastify, opts) {
|
|||
* 최근 트윗 동기화 (정기 실행)
|
||||
*/
|
||||
async function syncNewTweets(bot) {
|
||||
const options = { includeRetweets: bot.includeRetweets || false };
|
||||
const options = { includeRetweets: bot.includeRetweets || false, log: fastify.log };
|
||||
const { tweets, profile } = await fetchTweets(bot.nitterUrl, bot.username, options);
|
||||
|
||||
// 프로필 저장 (DB + 캐시)
|
||||
|
|
|
|||
|
|
@ -194,6 +194,8 @@ export function parseTweets(html, username, options = {}) {
|
|||
imageUrls,
|
||||
isRetweet,
|
||||
originalUsername,
|
||||
// 긴 트윗(280자 초과)이 …로 잘렸는지 여부 (hydrate 대상 판별용)
|
||||
truncated: /…\s*$/.test(text),
|
||||
url: isRetweet && originalUsername
|
||||
? `https://x.com/${originalUsername}/status/${id}`
|
||||
: `https://x.com/${username}/status/${id}`,
|
||||
|
|
@ -208,12 +210,7 @@ export function parseTweets(html, username, options = {}) {
|
|||
*/
|
||||
export async function fetchSingleTweet(nitterUrl, username, postId) {
|
||||
const url = `${nitterUrl}/${username}/status/${postId}`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`트윗을 찾을 수 없습니다 (${res.status})`);
|
||||
}
|
||||
|
||||
const res = await fetchWithTimeout(url);
|
||||
const html = await res.text();
|
||||
|
||||
// 메인 트윗 파싱 (main-tweet ~ replies 사이)
|
||||
|
|
@ -277,12 +274,43 @@ export async function fetchProfile(nitterUrl, username) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 잘린 트윗(…로 끝나는 긴 트윗)을 개별 상태 페이지에서 재요청해 전체 내용으로 교체
|
||||
* - 타임라인이 long tweet을 간헐적으로 잘라서 주는 경우 대비
|
||||
* - 재요청 결과가 더 길고 잘리지 않았을 때만 교체 (best-effort)
|
||||
* @param {string} nitterUrl - Nitter URL
|
||||
* @param {Array} tweets - parseTweets 결과
|
||||
* @param {string} username - 타임라인 사용자명 (리트윗 아닌 경우 fallback)
|
||||
* @param {object} log - 로거 (선택)
|
||||
*/
|
||||
async function hydrateTruncatedTweets(nitterUrl, tweets, username, log) {
|
||||
for (const tweet of tweets) {
|
||||
if (!tweet.truncated) continue;
|
||||
try {
|
||||
// status id는 전역 유일 → username 경로는 resolve에 영향 없음
|
||||
const author = tweet.originalUsername || username;
|
||||
const full = await fetchSingleTweet(nitterUrl, author, tweet.id);
|
||||
if (full?.text && full.text.length > tweet.text.length && !/…\s*$/.test(full.text)) {
|
||||
tweet.text = full.text;
|
||||
if (full.imageUrls?.length > 0) tweet.imageUrls = full.imageUrls;
|
||||
tweet.truncated = false;
|
||||
}
|
||||
} catch (err) {
|
||||
log?.warn?.(`[hydrate] 트윗 ${tweet.id} 재요청 실패: ${err.message}`);
|
||||
}
|
||||
// Nitter 부하 완화
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
return tweets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nitter에서 트윗 수집 (첫 페이지만)
|
||||
* @param {string} nitterUrl - Nitter URL
|
||||
* @param {string} username - 사용자명
|
||||
* @param {object} options - 옵션
|
||||
* @param {boolean} options.includeRetweets - 리트윗 포함 여부
|
||||
* @param {object} options.log - 로거 (선택)
|
||||
*/
|
||||
export async function fetchTweets(nitterUrl, username, options = {}) {
|
||||
const url = `${nitterUrl}/${username}`;
|
||||
|
|
@ -295,6 +323,9 @@ export async function fetchTweets(nitterUrl, username, options = {}) {
|
|||
// 트윗 파싱
|
||||
const tweets = parseTweets(html, username, options);
|
||||
|
||||
// 잘린 긴 트윗 전체 내용 복원
|
||||
await hydrateTruncatedTweets(nitterUrl, tweets, username, options.log);
|
||||
|
||||
return { tweets, profile };
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +360,8 @@ export async function fetchAllTweets(nitterUrl, username, log, options = {}) {
|
|||
if (emptyCount >= 3) break;
|
||||
} else {
|
||||
emptyCount = 0;
|
||||
// 잘린 긴 트윗 전체 내용 복원
|
||||
await hydrateTruncatedTweets(nitterUrl, tweets, username, log);
|
||||
allTweets.push(...tweets);
|
||||
log?.info(` -> ${tweets.length}개 추출 (누적: ${allTweets.length})`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue