diff --git a/src/commands/bookmarks.ts b/src/commands/bookmarks.ts index fafc057..8a9bc20 100644 --- a/src/commands/bookmarks.ts +++ b/src/commands/bookmarks.ts @@ -11,6 +11,7 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi .option('--folder-id ', 'Bookmark folder (collection) id') .option('--all', 'Fetch all bookmarks (paged)') .option('--max-pages ', 'Stop after N pages when using --all') + .option('--cursor ', '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( @@ -21,6 +22,7 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi folderId?: string; all?: boolean; maxPages?: string; + cursor?: string; }) => { const opts = program.opts(); const timeoutMs = ctx.resolveTimeoutFromOptions(opts); @@ -42,7 +44,8 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi console.error(`${ctx.p('err')}--max-pages requires --all.`); process.exit(1); } - if (!cmdOpts.all && (!Number.isFinite(count) || count <= 0)) { + const usePagination = cmdOpts.all || cmdOpts.cursor; + if (!usePagination && (!Number.isFinite(count) || count <= 0)) { console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); process.exit(1); } @@ -59,18 +62,24 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi } const includeRaw = cmdOpts.jsonFull ?? false; const timelineOptions = { includeRaw }; - const paginationOptions = { includeRaw, maxPages }; + const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor }; const result = folderId - ? cmdOpts.all + ? usePagination ? await client.getAllBookmarkFolderTimeline(folderId, paginationOptions) : await client.getBookmarkFolderTimeline(folderId, count, timelineOptions) - : cmdOpts.all + : usePagination ? await client.getAllBookmarks(paginationOptions) : await client.getBookmarks(count, timelineOptions); if (result.success && result.tweets) { const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.'; - ctx.printTweets(result.tweets, { json: cmdOpts.json || cmdOpts.jsonFull, emptyMessage }); + const isJson = cmdOpts.json || cmdOpts.jsonFull; + if (isJson && result.nextCursor) { + // Output with nextCursor for pagination + console.log(JSON.stringify({ tweets: result.tweets, nextCursor: result.nextCursor }, null, 2)); + } else { + ctx.printTweets(result.tweets, { json: isJson, emptyMessage }); + } } else { console.error(`${ctx.p('err')}Failed to fetch bookmarks: ${result.error}`); process.exit(1); diff --git a/src/lib/twitter-client-timelines.ts b/src/lib/twitter-client-timelines.ts index d418241..825f7d0 100644 --- a/src/lib/twitter-client-timelines.ts +++ b/src/lib/twitter-client-timelines.ts @@ -13,6 +13,8 @@ export interface TimelineFetchOptions { /** Options for paged timeline fetch methods */ export interface TimelinePaginationOptions extends TimelineFetchOptions { maxPages?: number; + /** Starting cursor for pagination (resume from previous fetch) */ + cursor?: string; } export interface TwitterClientTimelineMethods { @@ -198,7 +200,7 @@ export function withTimelines(); const tweets: TweetData[] = []; - let cursor: string | undefined; + let cursor: string | undefined = options.cursor; let pagesFetched = 0; const { includeRaw = false, maxPages } = options; @@ -341,12 +343,13 @@ export function withTimelines= maxPages) { + cursor = page.cursor; break; } cursor = page.cursor; } - return { success: true, tweets }; + return { success: true, tweets, nextCursor: cursor }; } private async getBookmarkFolderTimelinePaged( @@ -358,7 +361,7 @@ export function withTimelines(); const tweets: TweetData[] = []; - let cursor: string | undefined; + let cursor: string | undefined = options.cursor; let pagesFetched = 0; const { includeRaw = false, maxPages } = options; @@ -517,12 +520,13 @@ export function withTimelines= maxPages) { + cursor = page.cursor; break; } cursor = page.cursor; } - return { success: true, tweets }; + return { success: true, tweets, nextCursor: cursor }; } private async fetchWithRetry(url: string, init: RequestInit): Promise { diff --git a/src/lib/twitter-client-types.ts b/src/lib/twitter-client-types.ts index 2334365..eb9da7f 100644 --- a/src/lib/twitter-client-types.ts +++ b/src/lib/twitter-client-types.ts @@ -241,6 +241,8 @@ export interface SearchResult { success: boolean; tweets?: TweetData[]; error?: string; + /** Cursor for fetching the next page of results */ + nextCursor?: string; } export interface CurrentUserResult { diff --git a/src/lib/twitter-client-utils.ts b/src/lib/twitter-client-utils.ts index 0a0eac5..616bbac 100644 --- a/src/lib/twitter-client-utils.ts +++ b/src/lib/twitter-client-utils.ts @@ -297,21 +297,78 @@ export function findTweetInInstructions( result?: GraphqlTweetResult; }; }; + item?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + items?: Array<{ + item?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + content?: { + itemContent?: { + tweet_results?: { + result?: GraphqlTweetResult; + }; + }; + }; + }>; }; }>; }> | undefined, - tweetId: string, -) { - if (!instructions) { - return undefined; - } + id: string, + options: MapTweetResultOptions, +): TweetData | undefined { + for (const instruction of instructions ?? []) { + for (const entry of instruction.entries ?? []) { + const content = entry.content; + if (!content) { + continue; + } - for (const instruction of instructions) { - for (const entry of instruction.entries || []) { - const result = entry.content?.itemContent?.tweet_results?.result; - if (result?.rest_id === tweetId) { - return result; + const itemTweet = content.itemContent?.tweet_results?.result; + const itemResult = itemTweet ? unwrapTweetResult(itemTweet) : undefined; + if (itemResult?.rest_id === id) { + return mapTweetResult(itemResult, options); + } + + const innerItemTweet = content.item?.itemContent?.tweet_results?.result; + const innerItemResult = innerItemTweet ? unwrapTweetResult(innerItemTweet) : undefined; + if (innerItemResult?.rest_id === id) { + return mapTweetResult(innerItemResult, options); + } + + for (const item of content.items ?? []) { + const itemTweetResult = item.item?.itemContent?.tweet_results?.result; + const itemResult = itemTweetResult ? unwrapTweetResult(itemTweetResult) : undefined; + if (itemResult?.rest_id === id) { + return mapTweetResult(itemResult, options); + } + + const itemContentResult = item.itemContent?.tweet_results?.result; + const itemContentTweet = itemContentResult ? unwrapTweetResult(itemContentResult) : undefined; + if (itemContentTweet?.rest_id === id) { + return mapTweetResult(itemContentTweet, options); + } + + const itemEntryResult = item.content?.itemContent?.tweet_results?.result; + const itemEntryTweet = itemEntryResult ? unwrapTweetResult(itemEntryResult) : undefined; + if (itemEntryTweet?.rest_id === id) { + return mapTweetResult(itemEntryTweet, options); + } } } }