fix: list timeline pagination (#30) (thanks @zheli)

This commit is contained in:
Peter Steinberger
2026-01-11 11:27:46 +00:00
parent ebcd1618f9
commit 6cf177f92f
11 changed files with 1751 additions and 1506 deletions
+65 -53
View File
@@ -83,59 +83,71 @@ export function registerListsCommand(program: Command, ctx: CliContext): void {
.option('--cursor <string>', 'Resume pagination from a cursor')
.option('--json', 'Output as JSON')
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (listIdOrUrl: string, cmdOpts: { count?: string; json?: boolean; jsonFull?: boolean; all?: boolean; maxPages?: string; cursor?: string }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
const count = Number.parseInt(cmdOpts.count || '20', 10);
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
.action(
async (
listIdOrUrl: string,
cmdOpts: {
count?: string;
json?: boolean;
jsonFull?: boolean;
all?: boolean;
maxPages?: string;
cursor?: string;
},
) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
const count = Number.parseInt(cmdOpts.count || '20', 10);
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
const listId = extractListId(listIdOrUrl);
if (!listId) {
console.error(`${ctx.p('err')}Invalid list ID or URL. Expected numeric ID or https://x.com/i/lists/<id>.`);
process.exit(2);
}
const usePagination = cmdOpts.all || cmdOpts.cursor || maxPages !== undefined;
if (!usePagination && (!Number.isFinite(count) || count <= 0)) {
console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`);
process.exit(1);
}
if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) {
console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`);
process.exit(1);
}
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
for (const warning of warnings) {
console.error(`${ctx.p('warn')}${warning}`);
}
if (!cookies.authToken || !cookies.ct0) {
console.error(`${ctx.p('err')}Missing required credentials`);
process.exit(1);
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const includeRaw = cmdOpts.jsonFull ?? false;
const timelineOptions = { includeRaw };
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
const result = usePagination
? await client.getAllListTimeline(listId, paginationOptions)
: await client.getListTimeline(listId, count, timelineOptions);
if (result.success && result.tweets) {
const isJson = cmdOpts.json || cmdOpts.jsonFull;
if (isJson && usePagination) {
console.log(JSON.stringify({ tweets: result.tweets, nextCursor: result.nextCursor ?? null }, null, 2));
} else {
ctx.printTweets(result.tweets, { json: isJson, emptyMessage: 'No tweets found in this list.' });
const listId = extractListId(listIdOrUrl);
if (!listId) {
console.error(`${ctx.p('err')}Invalid list ID or URL. Expected numeric ID or https://x.com/i/lists/<id>.`);
process.exit(2);
}
} else {
console.error(`${ctx.p('err')}Failed to fetch list timeline: ${result.error}`);
process.exit(1);
}
});
const usePagination = cmdOpts.all || cmdOpts.cursor || maxPages !== undefined;
if (!usePagination && (!Number.isFinite(count) || count <= 0)) {
console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`);
process.exit(1);
}
if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) {
console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`);
process.exit(1);
}
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
for (const warning of warnings) {
console.error(`${ctx.p('warn')}${warning}`);
}
if (!cookies.authToken || !cookies.ct0) {
console.error(`${ctx.p('err')}Missing required credentials`);
process.exit(1);
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const includeRaw = cmdOpts.jsonFull ?? false;
const timelineOptions = { includeRaw };
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
const result = usePagination
? await client.getAllListTimeline(listId, paginationOptions)
: await client.getListTimeline(listId, count, timelineOptions);
if (result.success && result.tweets) {
const isJson = cmdOpts.json || cmdOpts.jsonFull;
if (isJson && usePagination) {
console.log(JSON.stringify({ tweets: result.tweets, nextCursor: result.nextCursor ?? null }, null, 2));
} else {
ctx.printTweets(result.tweets, { json: isJson, emptyMessage: 'No tweets found in this list.' });
}
} else {
console.error(`${ctx.p('err')}Failed to fetch list timeline: ${result.error}`);
process.exit(1);
}
},
);
}
+3 -1
View File
@@ -458,19 +458,21 @@ export function withLists<TBase extends AbstractConstructor<TwitterClientBase>>(
}
pagesFetched += 1;
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 (!unlimited && tweets.length >= limit) {
break;
}
}
const pageCursor = page.cursor;
if (!pageCursor || pageCursor === cursor || page.tweets.length === 0) {
if (!pageCursor || pageCursor === cursor || page.tweets.length === 0 || added === 0) {
nextCursor = undefined;
break;
}