diff --git a/src/lib/twitter-client-types.ts b/src/lib/twitter-client-types.ts index efa963a..2334365 100644 --- a/src/lib/twitter-client-types.ts +++ b/src/lib/twitter-client-types.ts @@ -227,6 +227,8 @@ export interface TweetData { quotedTweet?: TweetData; // Media attachments (photos, videos, GIFs) media?: TweetMedia[]; + // Raw GraphQL tweet result (only when includeRaw is enabled) + _raw?: GraphqlTweetResult; } export interface GetTweetResult { diff --git a/src/lib/twitter-client-utils.ts b/src/lib/twitter-client-utils.ts index 229605d..0a0eac5 100644 --- a/src/lib/twitter-client-utils.ts +++ b/src/lib/twitter-client-utils.ts @@ -224,7 +224,19 @@ export function unwrapTweetResult(result: GraphqlTweetResult | undefined): Graph return result; } -export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDepth: number): TweetData | undefined { +export interface MapTweetResultOptions { + quoteDepth: number; + includeRaw?: boolean; +} + +export function mapTweetResult( + result: GraphqlTweetResult | undefined, + quoteDepthOrOptions: number | MapTweetResultOptions, +): TweetData | undefined { + const options: MapTweetResultOptions = + typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions; + const { quoteDepth, includeRaw = false } = options; + const userResult = result?.core?.user_results?.result; const userLegacy = userResult?.legacy; const userCore = userResult?.core; @@ -244,13 +256,13 @@ export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDept if (quoteDepth > 0) { const quotedResult = unwrapTweetResult(result.quoted_status_result?.result); if (quotedResult) { - quotedTweet = mapTweetResult(quotedResult, quoteDepth - 1); + quotedTweet = mapTweetResult(quotedResult, { quoteDepth: quoteDepth - 1, includeRaw }); } } const media = extractMedia(result); - return { + const tweetData: TweetData = { id: result.rest_id, text, createdAt: result.legacy?.created_at, @@ -267,6 +279,12 @@ export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDept quotedTweet, media, }; + + if (includeRaw) { + (tweetData as TweetData & { _raw: GraphqlTweetResult })._raw = result; + } + + return tweetData; } export function findTweetInInstructions( @@ -358,6 +376,11 @@ export function collectTweetResultsFromEntry(entry: { return results; } +export interface ParseTweetsOptions { + quoteDepth: number; + includeRaw?: boolean; +} + export function parseTweetsFromInstructions( instructions: | Array<{ @@ -400,8 +423,12 @@ export function parseTweetsFromInstructions( }>; }> | undefined, - quoteDepth: number, + quoteDepthOrOptions: number | ParseTweetsOptions, ): TweetData[] { + const options: ParseTweetsOptions = + typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions; + const { quoteDepth, includeRaw = false } = options; + const tweets: TweetData[] = []; const seen = new Set(); @@ -409,7 +436,7 @@ export function parseTweetsFromInstructions( for (const entry of instruction.entries ?? []) { const results = collectTweetResultsFromEntry(entry); for (const result of results) { - const mapped = mapTweetResult(result, quoteDepth); + const mapped = mapTweetResult(result, { quoteDepth, includeRaw }); if (!mapped || seen.has(mapped.id)) { continue; } diff --git a/tests/twitter-client.lists.test.ts b/tests/twitter-client.lists.test.ts index e8fa3a8..ddeafa3 100644 --- a/tests/twitter-client.lists.test.ts +++ b/tests/twitter-client.lists.test.ts @@ -185,6 +185,55 @@ describe('TwitterClient lists', () => { expect(result.lists?.[0].isPrivate).toBe(true); }); + it('handles missing mode', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => ({ + data: { + user: { + result: { + timeline: { + timeline: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + list: { + id_str: '9999', + name: 'No Mode', + mode: null, + }, + }, + }, + }, + ], + }, + ], + }, + }, + }, + }, + }, + }), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const clientPrivate = client as unknown as TwitterClientPrivate; + clientPrivate.getCurrentUser = async () => ({ + success: true, + user: { id: '12345', username: 'testuser', name: 'Test User' }, + }); + clientPrivate.getListOwnershipsQueryIds = async () => ['test']; + + const result = await client.getOwnedLists(100); + + expect(result.success).toBe(true); + expect(result.lists?.[0].isPrivate).toBe(false); + }); + it('returns empty array when no lists exist', async () => { mockFetch.mockResolvedValueOnce({ ok: true,