From 94773c66dc6e0cb68e85f44508b1b573a594a965 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 11:36:59 +0000 Subject: [PATCH] fix: harden home timeline pagination (#31) (thanks @odysseus0) --- CHANGELOG.md | 1 + src/lib/twitter-client-home.ts | 4 +- tests/twitter-client.home-timeline.test.ts | 93 +++++++++++++++++++++- 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8509607..df5755f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - `list-timeline` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#30) — thanks @zheli. +- `home` command for the "For You" and "Following" home timelines (#31) — thanks @odysseus0. ### Fixed - macOS cookie extraction now supports Brave keychain storage (#40) — thanks @gakonst. diff --git a/src/lib/twitter-client-home.ts b/src/lib/twitter-client-home.ts index 4f7794d..a4b5ada 100644 --- a/src/lib/twitter-client-home.ts +++ b/src/lib/twitter-client-home.ts @@ -163,18 +163,20 @@ export function withHome>( return { success: false, error: page.error }; } + let added = 0; for (const tweet of page.tweets) { if (seen.has(tweet.id)) { continue; } seen.add(tweet.id); tweets.push(tweet); + added += 1; if (tweets.length >= count) { break; } } - if (!page.cursor || page.cursor === cursor || page.tweets.length === 0) { + if (!page.cursor || page.cursor === cursor || page.tweets.length === 0 || added === 0) { break; } cursor = page.cursor; diff --git a/tests/twitter-client.home-timeline.test.ts b/tests/twitter-client.home-timeline.test.ts index 19cbba2..607e82a 100644 --- a/tests/twitter-client.home-timeline.test.ts +++ b/tests/twitter-client.home-timeline.test.ts @@ -62,7 +62,7 @@ describe('TwitterClient home timeline', () => { beforeEach(() => { mockFetch = vi.fn(); - global.fetch = mockFetch; + global.fetch = mockFetch as unknown as typeof fetch; }); describe('getHomeTimeline', () => { @@ -249,5 +249,96 @@ describe('TwitterClient home timeline', () => { expect(result.tweets?.[0].id).toBe('tweet1'); expect(result.tweets?.[1].id).toBe('tweet2'); }); + + it('stops when a page only returns duplicates', async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: { + home: { + home_timeline_urt: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + tweet_results: { + result: { + rest_id: 'tweet1', + legacy: { full_text: 'First tweet', created_at: 'Mon Jan 06 00:00:00 +0000 2025' }, + core: { + user_results: { + result: { rest_id: 'u1', legacy: { screen_name: 'user1', name: 'User 1' } }, + }, + }, + }, + }, + }, + }, + }, + { + content: { + cursorType: 'Bottom', + value: 'cursor123', + }, + }, + ], + }, + ], + }, + }, + }, + }), + }); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + data: { + home: { + home_timeline_urt: { + instructions: [ + { + entries: [ + { + content: { + itemContent: { + tweet_results: { + result: { + rest_id: 'tweet1', + legacy: { full_text: 'First tweet', created_at: 'Mon Jan 06 00:00:00 +0000 2025' }, + core: { + user_results: { + result: { rest_id: 'u1', legacy: { screen_name: 'user1', name: 'User 1' } }, + }, + }, + }, + }, + }, + }, + }, + { + content: { + cursorType: 'Bottom', + value: 'cursor456', + }, + }, + ], + }, + ], + }, + }, + }, + }), + }); + + const client = new TwitterClient({ cookies: validCookies }); + const result = await client.getHomeTimeline(5); + + expect(result.success).toBe(true); + expect(result.tweets).toHaveLength(1); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); }); });