From 7a7453d2d2eb29aa02bf554113b201674b45cb7a Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 28 Apr 2026 21:09:55 +0800 Subject: [PATCH] fix(twitter): handle unavailable quoted tweets --- lib/routes/twitter/api/web-api/utils.ts | 46 +++++++------- lib/routes/twitter/utils.ts | 19 +++--- lib/twitter-web-api-utils.test.ts | 83 +++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 32 deletions(-) create mode 100644 lib/twitter-web-api-utils.test.ts diff --git a/lib/routes/twitter/api/web-api/utils.ts b/lib/routes/twitter/api/web-api/utils.ts index 560211aab..522cdb147 100644 --- a/lib/routes/twitter/api/web-api/utils.ts +++ b/lib/routes/twitter/api/web-api/utils.ts @@ -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; diff --git a/lib/routes/twitter/utils.ts b/lib/routes/twitter/utils.ts index e1de97426..9eee3dfcc 100644 --- a/lib/routes/twitter/utils.ts +++ b/lib/routes/twitter/utils.ts @@ -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 += '
'; @@ -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: [ diff --git a/lib/twitter-web-api-utils.test.ts b/lib/twitter-web-api-utils.test.ts new file mode 100644 index 000000000..555b3b794 --- /dev/null +++ b/lib/twitter-web-api-utils.test.ts @@ -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) => ({ + 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(); + }); +});