fix(twitter): handle unavailable quoted tweets

This commit is contained in:
DIYgod 2026-04-28 21:09:55 +08:00
parent 8f1e11203d
commit 7a7453d2d2
3 changed files with 116 additions and 32 deletions

View File

@ -298,6 +298,26 @@ export const paginationTweets = async (endpoint: string, userId: number | undefi
return gridEntries || moduleItems || entries || [];
};
const getLegacyUser = (tweet: any) => tweet.core?.user_result?.result?.legacy || tweet.core?.user_results?.result?.legacy;
const getCoreUser = (tweet: any) => tweet.core?.user_result?.result?.core || tweet.core?.user_results?.result?.core;
const hydrateLegacyUser = (legacy: any, tweet: any) => {
legacy.user = getLegacyUser(tweet);
const coreUser = getCoreUser(tweet);
if (!legacy.user || !coreUser) {
return;
}
if (coreUser.name) {
legacy.user.name = coreUser.name;
}
if (coreUser.screen_name) {
legacy.user.screen_name = coreUser.screen_name;
}
};
export function gatherLegacyFromData(entries: any[], filterNested?: string[], userId?: number | string) {
const tweets: any[] = [];
const filteredEntries: any[] = [];
@ -327,32 +347,12 @@ export function gatherLegacyFromData(entries: any[], filterNested?: string[], us
if (!t?.legacy) {
continue;
}
t.legacy.user = t.core?.user_result?.result?.legacy || t.core?.user_results?.result?.legacy;
// Add name and screen_name from core to maintain compatibility
if (t.legacy.user && t.core?.user_results?.result?.core) {
const coreUser = t.core.user_results.result.core;
if (coreUser.name) {
t.legacy.user.name = coreUser.name;
}
if (coreUser.screen_name) {
t.legacy.user.screen_name = coreUser.screen_name;
}
}
hydrateLegacyUser(t.legacy, t);
t.legacy.id_str = t.rest_id; // avoid falling back to conversation_id_str elsewhere
const quote = t.quoted_status_result?.result?.tweet || t.quoted_status_result?.result;
if (quote) {
if (quote?.legacy) {
t.legacy.quoted_status = quote.legacy;
t.legacy.quoted_status.user = quote.core.user_result?.result?.legacy || quote.core.user_results?.result?.legacy;
// Add name and screen_name from core for quoted status user
if (t.legacy.quoted_status.user && quote.core?.user_results?.result?.core) {
const quoteCoreUser = quote.core.user_results.result.core;
if (quoteCoreUser.name) {
t.legacy.quoted_status.user.name = quoteCoreUser.name;
}
if (quoteCoreUser.screen_name) {
t.legacy.quoted_status.user.screen_name = quoteCoreUser.screen_name;
}
}
hydrateLegacyUser(t.legacy.quoted_status, quote);
}
if (t.note_tweet) {
const tmp = t.note_tweet.note_tweet_results.result;

View File

@ -210,7 +210,7 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
if (item.is_quote_status) {
const quoteData = item.quoted_status;
if (quoteData) {
if (quoteData?.user) {
quoteData.full_text = quoteData.full_text || quoteData.text;
const author = quoteData.user;
quote += '<div class="rsshub-quote">';
@ -416,14 +416,15 @@ const ProcessFeed = (ctx, { data = [] }, params = {}) => {
},
],
}) ||
(item.is_quote_status && {
links: [
{
url: `https://x.com/${item.quoted_status?.user?.screen_name}/status/${item.quoted_status?.id_str || item.quoted_status?.conversation_id_str}`,
type: 'quote',
},
],
}) ||
(item.is_quote_status &&
item.quoted_status?.user && {
links: [
{
url: `https://x.com/${item.quoted_status?.user?.screen_name}/status/${item.quoted_status?.id_str || item.quoted_status?.conversation_id_str}`,
type: 'quote',
},
],
}) ||
(item.in_reply_to_screen_name &&
item.in_reply_to_status_id_str && {
links: [

View File

@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest';
import { gatherLegacyFromData } from './routes/twitter/api/web-api/utils';
const buildUser = (name: string, screenName: string) => ({
core: {
name,
screen_name: screenName,
},
legacy: {
name: `${name} legacy`,
profile_image_url_https: `https://example.com/${screenName}.jpg`,
screen_name: `${screenName}_legacy`,
},
});
const buildTweetEntry = (id: string, quotedResult?: Record<string, any>) => ({
entryId: `tweet-${id}`,
content: {
itemContent: {
tweet_results: {
result: {
__typename: 'Tweet',
core: {
user_results: {
result: buildUser('Author', 'author'),
},
},
is_quote_status: Boolean(quotedResult),
legacy: {
conversation_id_str: id,
created_at: 'Sun Apr 26 14:01:56 +0000 2026',
entities: {
urls: [],
},
full_text: `tweet ${id}`,
is_quote_status: Boolean(quotedResult),
},
quoted_status_result: quotedResult && {
result: quotedResult,
},
rest_id: id,
},
},
},
},
});
describe('gatherLegacyFromData', () => {
it('skips quoted tombstones without dropping the timeline', () => {
const normalQuote = {
__typename: 'Tweet',
core: {
user_results: {
result: buildUser('Quoted', 'quoted'),
},
},
legacy: {
conversation_id_str: '10',
created_at: 'Sun Apr 26 13:00:00 +0000 2026',
entities: {
urls: [],
},
full_text: 'quoted tweet',
},
rest_id: '10',
};
const tombstoneQuote = {
__typename: 'TweetTombstone',
tombstone: {
text: {
text: 'This post is unavailable.',
},
},
};
const tweets = gatherLegacyFromData([buildTweetEntry('1', normalQuote), buildTweetEntry('2', tombstoneQuote)]);
expect(tweets).toHaveLength(2);
expect(tweets[0].quoted_status.user.screen_name).toBe('quoted');
expect(tweets[1].quoted_status).toBeUndefined();
});
});