fix: harden home timeline pagination (#31) (thanks @odysseus0)

This commit is contained in:
Peter Steinberger
2026-01-11 11:36:59 +00:00
parent 92742cac38
commit 94773c66dc
3 changed files with 96 additions and 2 deletions
+1
View File
@@ -4,6 +4,7 @@
### Added ### Added
- `list-timeline` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#30) — thanks @zheli. - `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 ### Fixed
- macOS cookie extraction now supports Brave keychain storage (#40) — thanks @gakonst. - macOS cookie extraction now supports Brave keychain storage (#40) — thanks @gakonst.
+3 -1
View File
@@ -163,18 +163,20 @@ export function withHome<TBase extends AbstractConstructor<TwitterClientBase>>(
return { success: false, error: page.error }; return { success: false, error: page.error };
} }
let added = 0;
for (const tweet of page.tweets) { for (const tweet of page.tweets) {
if (seen.has(tweet.id)) { if (seen.has(tweet.id)) {
continue; continue;
} }
seen.add(tweet.id); seen.add(tweet.id);
tweets.push(tweet); tweets.push(tweet);
added += 1;
if (tweets.length >= count) { if (tweets.length >= count) {
break; break;
} }
} }
if (!page.cursor || page.cursor === cursor || page.tweets.length === 0) { if (!page.cursor || page.cursor === cursor || page.tweets.length === 0 || added === 0) {
break; break;
} }
cursor = page.cursor; cursor = page.cursor;
+92 -1
View File
@@ -62,7 +62,7 @@ describe('TwitterClient home timeline', () => {
beforeEach(() => { beforeEach(() => {
mockFetch = vi.fn(); mockFetch = vi.fn();
global.fetch = mockFetch; global.fetch = mockFetch as unknown as typeof fetch;
}); });
describe('getHomeTimeline', () => { describe('getHomeTimeline', () => {
@@ -249,5 +249,96 @@ describe('TwitterClient home timeline', () => {
expect(result.tweets?.[0].id).toBe('tweet1'); expect(result.tweets?.[0].id).toBe('tweet1');
expect(result.tweets?.[1].id).toBe('tweet2'); 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);
});
}); });
}); });