fix(x-bot): OG 카드 엔티티 디코딩 + YouTube 이미지 보강
- 스크래퍼가 Nitter HTML 엔티티(/ & 등)를 디코딩하지 않아 본문/카드 제목에 [JP/EN]처럼 노출되던 문제 수정 (extractCard, extractTextFromHtml에 decodeEntities 적용) - resolveCard가 Nitter 카드에 제목만 있고 이미지가 없을 때 OG로 이미지를 보강하도록 변경 (YouTube 카드 이미지 누락 복구) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
3ba27c0100
commit
134f5836b7
2 changed files with 40 additions and 11 deletions
|
|
@ -58,16 +58,26 @@ async function xBotPlugin(fastify, opts) {
|
||||||
* - 비었거나 없으면 본문 첫 URL로 OG 직접 추출 (YouTube 등 복구)
|
* - 비었거나 없으면 본문 첫 URL로 OG 직접 추출 (YouTube 등 복구)
|
||||||
*/
|
*/
|
||||||
async function resolveCard(tweet) {
|
async function resolveCard(tweet) {
|
||||||
if (tweet.card && (tweet.card.title || tweet.card.image)) {
|
const nitter = tweet.card && (tweet.card.title || tweet.card.image) ? tweet.card : null;
|
||||||
return tweet.card;
|
// Nitter 카드에 이미지가 있으면 그대로 사용
|
||||||
}
|
if (nitter && nitter.image) return nitter;
|
||||||
const url = extractFirstUrl(tweet.text);
|
|
||||||
if (!url) return null;
|
// 이미지가 없으면 OG로 보강 (YouTube 등 Nitter가 카드 이미지를 안 주는 경우 복구)
|
||||||
try {
|
const url = (nitter && nitter.url) || extractFirstUrl(tweet.text);
|
||||||
return await fetchOgCard(url);
|
if (url) {
|
||||||
} catch {
|
try {
|
||||||
return null;
|
const og = await fetchOgCard(url);
|
||||||
|
if (og) {
|
||||||
|
// Nitter 카드가 있으면 제목/설명은 유지하되 OG 이미지로 보강
|
||||||
|
return nitter
|
||||||
|
? { ...nitter, image: og.image || null, description: nitter.description || og.description }
|
||||||
|
: og;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTweet(tweet, username) {
|
async function saveTweet(tweet, username) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,24 @@ async function fetchWithTimeout(url, timeout = FETCH_TIMEOUT) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML 엔티티 디코딩 (Nitter가 / & 등으로 이스케이프한 텍스트 복원)
|
||||||
|
*/
|
||||||
|
function decodeEntities(str) {
|
||||||
|
if (!str) return str;
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/�?39;/g, "'")
|
||||||
|
.replace(/'/gi, "'")
|
||||||
|
.replace(///gi, '/')
|
||||||
|
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
|
||||||
|
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
||||||
|
.replace(/ /g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 트윗 텍스트에서 첫 문단 추출 (title용)
|
* 트윗 텍스트에서 첫 문단 추출 (title용)
|
||||||
*/
|
*/
|
||||||
|
|
@ -77,7 +95,7 @@ export function extractCard(html) {
|
||||||
const hrefMatch = html.match(/<a class="card-container" href="([^"]+)"/);
|
const hrefMatch = html.match(/<a class="card-container" href="([^"]+)"/);
|
||||||
if (!hrefMatch) return null;
|
if (!hrefMatch) return null;
|
||||||
|
|
||||||
const stripTags = s => (s ? s.replace(/<[^>]+>/g, '').trim() : '');
|
const stripTags = s => (s ? decodeEntities(s.replace(/<[^>]+>/g, '').trim()) : '');
|
||||||
const titleMatch = html.match(/<h2 class="card-title">([\s\S]*?)<\/h2>/);
|
const titleMatch = html.match(/<h2 class="card-title">([\s\S]*?)<\/h2>/);
|
||||||
const descMatch = html.match(/<p class="card-description">([\s\S]*?)<\/p>/);
|
const descMatch = html.match(/<p class="card-description">([\s\S]*?)<\/p>/);
|
||||||
const destMatch = html.match(/<span class="card-destination">([\s\S]*?)<\/span>/);
|
const destMatch = html.match(/<span class="card-destination">([\s\S]*?)<\/span>/);
|
||||||
|
|
@ -158,7 +176,7 @@ export function extractProfile(html) {
|
||||||
* 트윗 HTML 컨텐츠에서 텍스트 추출 (링크는 원본 URL 사용)
|
* 트윗 HTML 컨텐츠에서 텍스트 추출 (링크는 원본 URL 사용)
|
||||||
*/
|
*/
|
||||||
function extractTextFromHtml(html) {
|
function extractTextFromHtml(html) {
|
||||||
return html
|
const stripped = html
|
||||||
.replace(/<br\s*\/?>/g, '\n')
|
.replace(/<br\s*\/?>/g, '\n')
|
||||||
// <a> 태그: href에서 원본 URL 추출 (외부 링크만)
|
// <a> 태그: href에서 원본 URL 추출 (외부 링크만)
|
||||||
.replace(/<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/g, (match, href, text) => {
|
.replace(/<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/g, (match, href, text) => {
|
||||||
|
|
@ -176,6 +194,7 @@ function extractTextFromHtml(html) {
|
||||||
})
|
})
|
||||||
.replace(/<[^>]+>/g, '')
|
.replace(/<[^>]+>/g, '')
|
||||||
.trim();
|
.trim();
|
||||||
|
return decodeEntities(stripped);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue