From c90d3f9ab4be4093478b2dd8e4ad9b3236c232ea Mon Sep 17 00:00:00 2001 From: Alex Hillman Date: Wed, 31 Dec 2025 21:55:02 +0000 Subject: [PATCH 1/6] Add native quote tweet support to TweetData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add quoted_status_result to GraphqlTweetResult type - Add quotedTweet field to TweetData interface - Extract and map quoted tweets in mapTweetResult() - Works for getTweet, getBookmarks, search, etc. - Document JSON output schema in README 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 18 ++++++++++++++++++ src/lib/twitter-client.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/README.md b/README.md index 231976b..0158651 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,24 @@ Environment shortcuts: - `read` returns full text for Notes and Articles when present. - Use `--plain` for stable, script-friendly output (no emoji, no color). +### JSON Schema + +When using `--json`, tweet objects include: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Tweet ID | +| `text` | string | Full tweet text (includes Note/Article content when present) | +| `author` | object | `{ username, name }` | +| `authorId` | string | Author's user ID | +| `createdAt` | string | Timestamp | +| `replyCount` | number | Number of replies | +| `retweetCount` | number | Number of retweets | +| `likeCount` | number | Number of likes | +| `conversationId` | string | Thread conversation ID | +| `inReplyToStatusId` | string? | Parent tweet ID (present if this is a reply) | +| `quotedTweet` | object? | Embedded quote tweet with `{ id, text, author, authorId }` (present if this is a quote tweet) | + ## Query IDs (GraphQL) X rotates GraphQL “query IDs” frequently. Each GraphQL operation is addressed as: diff --git a/src/lib/twitter-client.ts b/src/lib/twitter-client.ts index e5f5379..7a957b7 100644 --- a/src/lib/twitter-client.ts +++ b/src/lib/twitter-client.ts @@ -180,6 +180,9 @@ type GraphqlTweetResult = { }>; }>; }; + quoted_status_result?: { + result?: GraphqlTweetResult; + }; }; export type TweetResult = @@ -212,6 +215,15 @@ export interface TweetData { likeCount?: number; conversationId?: string; inReplyToStatusId?: string; + quotedTweet?: { + id: string; + text: string; + author: { + username: string; + name: string; + }; + authorId?: string; + }; } export interface GetTweetResult { @@ -686,6 +698,29 @@ export class TwitterClient { const text = this.extractTweetText(result); if (!text) return undefined; + // Extract quoted tweet if present + let quotedTweet: TweetData['quotedTweet'] = undefined; + const quotedResult = result.quoted_status_result?.result; + if (quotedResult?.rest_id) { + const quotedUserResult = quotedResult?.core?.user_results?.result; + const quotedUserLegacy = quotedUserResult?.legacy; + const quotedUserCore = quotedUserResult?.core; + const quotedUsername = quotedUserLegacy?.screen_name ?? quotedUserCore?.screen_name; + const quotedName = quotedUserLegacy?.name ?? quotedUserCore?.name ?? quotedUsername; + const quotedText = this.extractTweetText(quotedResult); + if (quotedUsername && quotedText) { + quotedTweet = { + id: quotedResult.rest_id, + text: quotedText, + author: { + username: quotedUsername, + name: quotedName || quotedUsername, + }, + authorId: quotedUserResult?.rest_id, + }; + } + } + return { id: result.rest_id, text, @@ -700,6 +735,7 @@ export class TwitterClient { name: name || username, }, authorId: userId, + quotedTweet, }; } From 071953e9f43fd13073e48c7e183bcd058ac4c17f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 10:45:15 +0100 Subject: [PATCH 2/6] feat: add quote depth option for quoted tweets --- src/index.ts | 42 ++++++++++++++++++++++------- src/lib/twitter-client.ts | 56 ++++++++++++++++++--------------------- 2 files changed, 59 insertions(+), 39 deletions(-) diff --git a/src/index.ts b/src/index.ts index 931064a..86fc042 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,6 +99,7 @@ type BirdConfig = { firefoxProfile?: string; cookieSource?: CookieSource | CookieSource[]; timeoutMs?: number; + quoteDepth?: number; }; function readConfigFile(path: string): Partial { @@ -180,6 +181,7 @@ program .option('--media ', 'Attach media file (repeatable, up to 4 images or 1 video)', collect, []) .option('--alt ', 'Alt text for the corresponding --media (repeatable)', collect, []) .option('--timeout ', 'Request timeout in milliseconds') + .option('--quote-depth ', 'Max quoted tweet depth (default: 1; 0 disables)') .option('--plain', 'Plain output (stable, no emoji, no color)') .option('--no-emoji', 'Disable emoji output') .option('--no-color', 'Disable ANSI colors (or set NO_COLOR)'); @@ -224,6 +226,19 @@ function resolveTimeoutFromOptions(options: { timeout?: string | number }): numb return resolveTimeoutMs(options.timeout, config.timeoutMs, process.env.BIRD_TIMEOUT_MS); } +function resolveQuoteDepth(...values: Array): number | undefined { + for (const value of values) { + if (value === undefined || value === null || value === '') continue; + const parsed = typeof value === 'number' ? value : Number.parseInt(value, 10); + if (Number.isFinite(parsed) && parsed >= 0) return Math.floor(parsed); + } + return undefined; +} + +function resolveQuoteDepthFromOptions(options: { quoteDepth?: string | number }): number | undefined { + return resolveQuoteDepth(options.quoteDepth, config.quoteDepth, process.env.BIRD_QUOTE_DEPTH); +} + function detectMime(path: string): string | null { const ext = path.toLowerCase(); if (ext.endsWith('.jpg') || ext.endsWith('.jpeg')) return 'image/jpeg'; @@ -364,6 +379,7 @@ program .action(async (text: string) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); let media: MediaSpec[] = []; try { media = loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] }); @@ -387,7 +403,7 @@ program console.error(`${l('source')}${cookies.source}`); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); let mediaIds: string[] | undefined; if (media.length > 0) { const uploaded: string[] = []; @@ -422,6 +438,7 @@ program .action(async (tweetIdOrUrl: string, text: string) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); let media: MediaSpec[] = []; try { media = loadMedia({ media: opts.media ?? [], alts: opts.alt ?? [] }); @@ -448,7 +465,7 @@ program console.error(`${p('info')}Replying to tweet: ${tweetId}`); - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); let mediaIds: string[] | undefined; if (media.length > 0) { const uploaded: string[] = []; @@ -483,6 +500,7 @@ program .action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const tweetId = extractTweetId(tweetIdOrUrl); @@ -497,7 +515,7 @@ program process.exit(1); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.getTweet(tweetId); if (result.success && result.tweet) { @@ -526,6 +544,7 @@ program .action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const tweetId = extractTweetId(tweetIdOrUrl); const { cookies, warnings } = await resolveCredentialsFromOptions(opts); @@ -539,7 +558,7 @@ program process.exit(1); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.getReplies(tweetId); if (result.success && result.tweets) { @@ -559,6 +578,7 @@ program .action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const tweetId = extractTweetId(tweetIdOrUrl); const { cookies, warnings } = await resolveCredentialsFromOptions(opts); @@ -572,7 +592,7 @@ program process.exit(1); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.getThread(tweetId); if (result.success && result.tweets) { @@ -593,6 +613,7 @@ program .action(async (query: string, cmdOpts: { count?: string; json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const count = Number.parseInt(cmdOpts.count || '10', 10); const { cookies, warnings } = await resolveCredentialsFromOptions(opts); @@ -606,7 +627,7 @@ program process.exit(1); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.search(query, count); if (result.success && result.tweets) { @@ -627,6 +648,7 @@ program .action(async (cmdOpts: { user?: string; count?: string; json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const count = Number.parseInt(cmdOpts.count || '10', 10); const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user); @@ -648,7 +670,7 @@ program process.exit(1); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); if (!query) { const who = await client.getCurrentUser(); @@ -682,6 +704,7 @@ program .action(async (cmdOpts: { count?: string; json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const count = Number.parseInt(cmdOpts.count || '20', 10); const { cookies, warnings } = await resolveCredentialsFromOptions(opts); @@ -695,7 +718,7 @@ program process.exit(1); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.getBookmarks(count); if (result.success && result.tweets) { @@ -713,6 +736,7 @@ program .action(async () => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); + const quoteDepth = resolveQuoteDepthFromOptions(opts); const { cookies, warnings } = await resolveCredentialsFromOptions(opts); @@ -729,7 +753,7 @@ program console.error(`${l('source')}${cookies.source}`); } - const client = new TwitterClient({ cookies, timeoutMs }); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.getCurrentUser(); const credentialSource = cookies.source ?? 'env/auto-detected cookies'; diff --git a/src/lib/twitter-client.ts b/src/lib/twitter-client.ts index 7a957b7..96c20e9 100644 --- a/src/lib/twitter-client.ts +++ b/src/lib/twitter-client.ts @@ -35,7 +35,14 @@ const QUERY_IDS: Record = { const TARGET_QUERY_ID_OPERATIONS = Object.keys(FALLBACK_QUERY_IDS) as Array; +function normalizeQuoteDepth(value?: number): number { + if (value === undefined || value === null) return 1; + if (!Number.isFinite(value)) return 1; + return Math.max(0, Math.floor(value)); +} + type GraphqlTweetResult = { + __typename?: string; rest_id?: string; legacy?: { full_text?: string; @@ -180,6 +187,7 @@ type GraphqlTweetResult = { }>; }>; }; + tweet?: GraphqlTweetResult; quoted_status_result?: { result?: GraphqlTweetResult; }; @@ -215,15 +223,8 @@ export interface TweetData { likeCount?: number; conversationId?: string; inReplyToStatusId?: string; - quotedTweet?: { - id: string; - text: string; - author: { - username: string; - name: string; - }; - authorId?: string; - }; + // Optional quoted tweet; depth controlled by quoteDepth (default: 1). + quotedTweet?: TweetData; } export interface GetTweetResult { @@ -252,6 +253,8 @@ export interface TwitterClientOptions { cookies: TwitterCookies; userAgent?: string; timeoutMs?: number; + // Max depth for quoted tweets (0 disables). Defaults to 1. + quoteDepth?: number; } interface CreateTweetResponse { @@ -276,6 +279,7 @@ export class TwitterClient { private cookieHeader: string; private userAgent: string; private timeoutMs?: number; + private quoteDepth: number; private clientUuid: string; private clientDeviceId: string; private clientUserId?: string; @@ -291,6 +295,7 @@ export class TwitterClient { options.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; this.timeoutMs = options.timeoutMs; + this.quoteDepth = normalizeQuoteDepth(options.quoteDepth); this.clientUuid = randomUUID(); this.clientDeviceId = randomUUID(); } @@ -686,7 +691,13 @@ export class TwitterClient { ); } - private mapTweetResult(result: GraphqlTweetResult | undefined): TweetData | undefined { + private unwrapTweetResult(result: GraphqlTweetResult | undefined): GraphqlTweetResult | undefined { + if (!result) return undefined; + if (result.tweet) return result.tweet; + return result; + } + + private mapTweetResult(result: GraphqlTweetResult | undefined, quoteDepth = this.quoteDepth): TweetData | undefined { const userResult = result?.core?.user_results?.result; const userLegacy = userResult?.legacy; const userCore = userResult?.core; @@ -698,26 +709,11 @@ export class TwitterClient { const text = this.extractTweetText(result); if (!text) return undefined; - // Extract quoted tweet if present - let quotedTweet: TweetData['quotedTweet'] = undefined; - const quotedResult = result.quoted_status_result?.result; - if (quotedResult?.rest_id) { - const quotedUserResult = quotedResult?.core?.user_results?.result; - const quotedUserLegacy = quotedUserResult?.legacy; - const quotedUserCore = quotedUserResult?.core; - const quotedUsername = quotedUserLegacy?.screen_name ?? quotedUserCore?.screen_name; - const quotedName = quotedUserLegacy?.name ?? quotedUserCore?.name ?? quotedUsername; - const quotedText = this.extractTweetText(quotedResult); - if (quotedUsername && quotedText) { - quotedTweet = { - id: quotedResult.rest_id, - text: quotedText, - author: { - username: quotedUsername, - name: quotedName || quotedUsername, - }, - authorId: quotedUserResult?.rest_id, - }; + let quotedTweet: TweetData | undefined; + if (quoteDepth > 0) { + const quotedResult = this.unwrapTweetResult(result.quoted_status_result?.result); + if (quotedResult) { + quotedTweet = this.mapTweetResult(quotedResult, quoteDepth - 1); } } From df6b92e39c97ee7584ff17da351b58aaf98ffcf8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 10:45:20 +0100 Subject: [PATCH 3/6] test: cover quoted tweet depth --- tests/twitter-client.test.ts | 91 +++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/tests/twitter-client.test.ts b/tests/twitter-client.test.ts index 5a17e9f..645f665 100644 --- a/tests/twitter-client.test.ts +++ b/tests/twitter-client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { TwitterClient } from '../src/lib/twitter-client.js'; +import { type TweetData, TwitterClient } from '../src/lib/twitter-client.js'; describe('TwitterClient', () => { const originalFetch = global.fetch; @@ -952,6 +952,95 @@ describe('TwitterClient', () => { }); }); + describe('quoted tweets', () => { + const makeTweetResult = ( + id: string, + text: string, + username = `user${id}`, + name = `User ${id}`, + ): Record => ({ + rest_id: id, + legacy: { + full_text: text, + created_at: '2024-01-01T00:00:00Z', + reply_count: 0, + retweet_count: 0, + favorite_count: 0, + conversation_id_str: id, + }, + core: { + user_results: { + result: { + rest_id: `u${id}`, + legacy: { screen_name: username, name }, + }, + }, + }, + }); + + it('includes one level of quoted tweet by default', () => { + const quoted = makeTweetResult('2', 'quoted'); + const root = makeTweetResult('1', 'root'); + root.quoted_status_result = { result: quoted }; + + const client = new TwitterClient({ cookies: validCookies }); + const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( + root, + ); + + expect(mapped?.quotedTweet?.id).toBe('2'); + expect(mapped?.quotedTweet?.quotedTweet).toBeUndefined(); + }); + + it('honors quoteDepth = 0', () => { + const quoted = makeTweetResult('2', 'quoted'); + const root = makeTweetResult('1', 'root'); + root.quoted_status_result = { result: quoted }; + + const client = new TwitterClient({ cookies: validCookies, quoteDepth: 0 }); + const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( + root, + ); + + expect(mapped?.quotedTweet).toBeUndefined(); + }); + + it('recurses when quoteDepth > 1', () => { + const quoted2 = makeTweetResult('3', 'quoted2'); + const quoted1 = makeTweetResult('2', 'quoted1'); + quoted1.quoted_status_result = { result: quoted2 }; + const root = makeTweetResult('1', 'root'); + root.quoted_status_result = { result: quoted1 }; + + const client = new TwitterClient({ cookies: validCookies, quoteDepth: 2 }); + const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( + root, + ); + + expect(mapped?.quotedTweet?.id).toBe('2'); + expect(mapped?.quotedTweet?.quotedTweet?.id).toBe('3'); + expect(mapped?.quotedTweet?.quotedTweet?.quotedTweet).toBeUndefined(); + }); + + it('unwraps quoted tweet visibility wrappers', () => { + const quoted = makeTweetResult('2', 'quoted'); + const root = makeTweetResult('1', 'root'); + root.quoted_status_result = { + result: { + __typename: 'TweetWithVisibilityResults', + tweet: quoted, + }, + }; + + const client = new TwitterClient({ cookies: validCookies }); + const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( + root, + ); + + expect(mapped?.quotedTweet?.id).toBe('2'); + }); + }); + describe('search', () => { let mockFetch: ReturnType; From 529403e38669a58ce86f63fcd718bf6da59086fe Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 10:45:25 +0100 Subject: [PATCH 4/6] docs: document quoted tweet depth --- CHANGELOG.md | 3 +++ README.md | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661a665..05c54ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.4.1 — Unreleased +### Added +- Quoted tweet data in JSON output + `--quote-depth` (thanks @alexknowshtml). + ### Changed - Cookie extraction now uses `@steipete/sweet-cookie` (drops `sqlite3` CLI + custom browser readers in `bird`). diff --git a/README.md b/README.md index 0158651..12879fd 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ bird query-ids --fresh Global options: - `--timeout `: abort requests after the given timeout (milliseconds). +- `--quote-depth `: max quoted tweet depth in JSON output (default: 1; 0 disables). - `--plain`: stable output (no emoji, no color). - `--no-emoji`: disable emoji output. - `--no-color`: disable ANSI colors (or set `NO_COLOR=1`). @@ -108,12 +109,14 @@ Example `~/.config/bird/config.json5`: // Cookie source order for browser extraction (string or array) cookieSource: ["firefox", "safari"], firefoxProfile: "default-release", - timeoutMs: 20000 + timeoutMs: 20000, + quoteDepth: 1 } ``` Environment shortcuts: - `BIRD_TIMEOUT_MS` +- `BIRD_QUOTE_DEPTH` ## Output @@ -130,14 +133,14 @@ When using `--json`, tweet objects include: | `id` | string | Tweet ID | | `text` | string | Full tweet text (includes Note/Article content when present) | | `author` | object | `{ username, name }` | -| `authorId` | string | Author's user ID | +| `authorId` | string? | Author's user ID | | `createdAt` | string | Timestamp | | `replyCount` | number | Number of replies | | `retweetCount` | number | Number of retweets | | `likeCount` | number | Number of likes | | `conversationId` | string | Thread conversation ID | | `inReplyToStatusId` | string? | Parent tweet ID (present if this is a reply) | -| `quotedTweet` | object? | Embedded quote tweet with `{ id, text, author, authorId }` (present if this is a quote tweet) | +| `quotedTweet` | object? | Embedded quote tweet (same schema; depth controlled by `--quote-depth`) | ## Query IDs (GraphQL) From 68ec1d67bf492cb6709d7ea60bb4154bc1b6a5b2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 10:47:26 +0100 Subject: [PATCH 5/6] docs: update changelog for quoted tweets --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 296c4c0..07adda4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - `likes` command to list your liked tweets (thanks @swairshah). +- Quoted tweet data in JSON output + `--quote-depth` (thanks @alexknowshtml). ### Changed - Query ID updater now tracks the Likes GraphQL operation. @@ -14,9 +15,6 @@ - `bookmarks` command to list your bookmarked tweets. - `bookmarks --folder-id` to fetch bookmark folders (thanks @tylerseymour). -### Added -- Quoted tweet data in JSON output + `--quote-depth` (thanks @alexknowshtml). - ### Changed - Cookie extraction now uses `@steipete/sweet-cookie` (drops `sqlite3` CLI + custom browser readers in `bird`). - Query ID updater now tracks the Bookmarks GraphQL operation. From 9b7dab76439d7db2ad25978beb49cece8c2f5793 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 10:48:34 +0100 Subject: [PATCH 6/6] style: satisfy lint for quote depth --- src/index.ts | 8 ++++++-- src/lib/twitter-client.ts | 16 ++++++++++++---- tests/twitter-client.test.ts | 24 ++++++++++++------------ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index c969b0f..099f7bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -245,9 +245,13 @@ function resolveTimeoutFromOptions(options: { timeout?: string | number }): numb function resolveQuoteDepth(...values: Array): number | undefined { for (const value of values) { - if (value === undefined || value === null || value === '') continue; + if (value === undefined || value === null || value === '') { + continue; + } const parsed = typeof value === 'number' ? value : Number.parseInt(value, 10); - if (Number.isFinite(parsed) && parsed >= 0) return Math.floor(parsed); + if (Number.isFinite(parsed) && parsed >= 0) { + return Math.floor(parsed); + } } return undefined; } diff --git a/src/lib/twitter-client.ts b/src/lib/twitter-client.ts index 6ef1878..c84eeaa 100644 --- a/src/lib/twitter-client.ts +++ b/src/lib/twitter-client.ts @@ -42,8 +42,12 @@ const QUERY_IDS: Record = { const TARGET_QUERY_ID_OPERATIONS = Object.keys(FALLBACK_QUERY_IDS) as Array; function normalizeQuoteDepth(value?: number): number { - if (value === undefined || value === null) return 1; - if (!Number.isFinite(value)) return 1; + if (value === undefined || value === null) { + return 1; + } + if (!Number.isFinite(value)) { + return 1; + } return Math.max(0, Math.floor(value)); } @@ -729,8 +733,12 @@ export class TwitterClient { } private unwrapTweetResult(result: GraphqlTweetResult | undefined): GraphqlTweetResult | undefined { - if (!result) return undefined; - if (result.tweet) return result.tweet; + if (!result) { + return undefined; + } + if (result.tweet) { + return result.tweet; + } return result; } diff --git a/tests/twitter-client.test.ts b/tests/twitter-client.test.ts index 5b86f1f..15e6128 100644 --- a/tests/twitter-client.test.ts +++ b/tests/twitter-client.test.ts @@ -992,9 +992,9 @@ describe('TwitterClient', () => { root.quoted_status_result = { result: quoted }; const client = new TwitterClient({ cookies: validCookies }); - const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( - root, - ); + const mapped = ( + client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined } + ).mapTweetResult(root); expect(mapped?.quotedTweet?.id).toBe('2'); expect(mapped?.quotedTweet?.quotedTweet).toBeUndefined(); @@ -1006,9 +1006,9 @@ describe('TwitterClient', () => { root.quoted_status_result = { result: quoted }; const client = new TwitterClient({ cookies: validCookies, quoteDepth: 0 }); - const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( - root, - ); + const mapped = ( + client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined } + ).mapTweetResult(root); expect(mapped?.quotedTweet).toBeUndefined(); }); @@ -1021,9 +1021,9 @@ describe('TwitterClient', () => { root.quoted_status_result = { result: quoted1 }; const client = new TwitterClient({ cookies: validCookies, quoteDepth: 2 }); - const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( - root, - ); + const mapped = ( + client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined } + ).mapTweetResult(root); expect(mapped?.quotedTweet?.id).toBe('2'); expect(mapped?.quotedTweet?.quotedTweet?.id).toBe('3'); @@ -1041,9 +1041,9 @@ describe('TwitterClient', () => { }; const client = new TwitterClient({ cookies: validCookies }); - const mapped = (client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined }).mapTweetResult( - root, - ); + const mapped = ( + client as unknown as { mapTweetResult: (result: unknown) => TweetData | undefined } + ).mapTweetResult(root); expect(mapped?.quotedTweet?.id).toBe('2'); });