From 1b2afd5acb3f6b39e96a12a799ffee5cb2fd5915 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 19 Jan 2026 04:51:54 +0000 Subject: [PATCH] refactor: normalize about output + retry helper --- src/commands/users.ts | 48 ++++++++++++++++----------- src/lib/twitter-client-base.ts | 12 +++++++ src/lib/twitter-client-user-lookup.ts | 18 +++------- src/lib/twitter-client-users.ts | 36 ++++++-------------- 4 files changed, 54 insertions(+), 60 deletions(-) diff --git a/src/commands/users.ts b/src/commands/users.ts index cef4888..9b5cf5d 100644 --- a/src/commands/users.ts +++ b/src/commands/users.ts @@ -2,9 +2,27 @@ import type { Command } from 'commander'; import type { CliContext } from '../cli/shared.js'; import { normalizeHandle } from '../lib/normalize-handle.js'; import { TwitterClient } from '../lib/twitter-client.js'; -import type { TwitterUser } from '../lib/twitter-client-types.js'; +import type { AboutAccountProfile, TwitterUser } from '../lib/twitter-client-types.js'; -const LEADING_AT_REGEX = /^@+/; +function formatAboutProfile(profile: AboutAccountProfile, ctx: CliContext, handle: string): string[] { + const lines: string[] = [`${ctx.p('info')}Account information for @${handle}:`]; + if (profile.accountBasedIn) { + lines.push(` Account based in: ${profile.accountBasedIn}`); + } + if (profile.createdCountryAccurate !== undefined) { + lines.push(` Creation country accurate: ${profile.createdCountryAccurate ? 'Yes' : 'No'}`); + } + if (profile.locationAccurate !== undefined) { + lines.push(` Location accurate: ${profile.locationAccurate ? 'Yes' : 'No'}`); + } + if (profile.source) { + lines.push(`${ctx.l('source')}${profile.source}`); + } + if (profile.learnMoreUrl) { + lines.push(` Learn more: ${profile.learnMoreUrl}`); + } + return lines; +} type PagedUsersResult = { success: boolean; @@ -294,6 +312,11 @@ export function registerUserCommands(program: Command, ctx: CliContext): void { const timeoutMs = ctx.resolveTimeoutFromOptions(opts); const normalizedHandle = normalizeHandle(username); + if (!normalizedHandle) { + console.error(`${ctx.p('err')}Invalid username: ${username}`); + process.exit(1); + } + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); for (const warning of warnings) { @@ -306,29 +329,14 @@ export function registerUserCommands(program: Command, ctx: CliContext): void { } const client = new TwitterClient({ cookies, timeoutMs }); - const result = await client.getUserAboutAccount(username); + const result = await client.getUserAboutAccount(normalizedHandle); if (result.success && result.aboutProfile) { if (cmdOpts.json) { console.log(JSON.stringify(result.aboutProfile, null, 2)); } else { - const profile = result.aboutProfile; - const displayHandle = normalizedHandle ?? username.replace(LEADING_AT_REGEX, ''); - console.log(`${ctx.p('info')}Account information for @${displayHandle}:`); - if (profile.accountBasedIn) { - console.log(` Account based in: ${profile.accountBasedIn}`); - } - if (profile.createdCountryAccurate !== undefined) { - console.log(` Creation country accurate: ${profile.createdCountryAccurate ? 'Yes' : 'No'}`); - } - if (profile.locationAccurate !== undefined) { - console.log(` Location accurate: ${profile.locationAccurate ? 'Yes' : 'No'}`); - } - if (profile.source) { - console.log(`${ctx.l('source')}${profile.source}`); - } - if (profile.learnMoreUrl) { - console.log(` Learn more: ${profile.learnMoreUrl}`); + for (const line of formatAboutProfile(result.aboutProfile, ctx, normalizedHandle)) { + console.log(line); } } } else { diff --git a/src/lib/twitter-client-base.ts b/src/lib/twitter-client-base.ts index 4f92101..d0c39bc 100644 --- a/src/lib/twitter-client-base.ts +++ b/src/lib/twitter-client-base.ts @@ -49,6 +49,18 @@ export abstract class TwitterClientBase { } } + protected async withRefreshedQueryIdsOn404( + attempt: () => Promise, + ): Promise<{ result: T; refreshed: boolean }> { + const firstAttempt = await attempt(); + if (firstAttempt.success || !firstAttempt.had404) { + return { result: firstAttempt, refreshed: false }; + } + await this.refreshQueryIds(); + const secondAttempt = await attempt(); + return { result: secondAttempt, refreshed: true }; + } + protected async getTweetDetailQueryIds(): Promise { const primary = await this.getQueryId('TweetDetail'); return Array.from(new Set([primary, '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g'])); diff --git a/src/lib/twitter-client-user-lookup.ts b/src/lib/twitter-client-user-lookup.ts index ff349f1..97be9f9 100644 --- a/src/lib/twitter-client-user-lookup.ts +++ b/src/lib/twitter-client-user-lookup.ts @@ -307,21 +307,11 @@ export function withUserLookup>( return { success: false as const, error: lastError ?? 'Unknown error fetching following', had404 }; }; - const firstAttempt = await tryOnce(); - if (firstAttempt.success) { - return { success: true, users: firstAttempt.users, nextCursor: firstAttempt.nextCursor }; + const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce); + if (result.success) { + return { success: true, users: result.users, nextCursor: result.nextCursor }; } - if (firstAttempt.had404) { - await this.refreshQueryIds(); - const secondAttempt = await tryOnce(); - if (secondAttempt.success) { - return { success: true, users: secondAttempt.users, nextCursor: secondAttempt.nextCursor }; - } - + if (refreshed) { // GraphQL Following can also return 404 (queryId churn / endpoint flakiness). // Fallback to the internal v1.1 REST endpoint used by the web client through the relay. const restAttempt = await this.getFollowingViaRest(userId, count, cursor); if (restAttempt.success) { return restAttempt; } - - return { success: false, error: secondAttempt.error }; } - return { success: false, error: firstAttempt.error }; + return { success: false, error: result.error }; } /** @@ -436,29 +428,21 @@ export function withUsers>( return { success: false as const, error: lastError ?? 'Unknown error fetching followers', had404 }; }; - const firstAttempt = await tryOnce(); - if (firstAttempt.success) { - return { success: true, users: firstAttempt.users, nextCursor: firstAttempt.nextCursor }; + const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce); + if (result.success) { + return { success: true, users: result.users, nextCursor: result.nextCursor }; } - if (firstAttempt.had404) { - await this.refreshQueryIds(); - const secondAttempt = await tryOnce(); - if (secondAttempt.success) { - return { success: true, users: secondAttempt.users, nextCursor: secondAttempt.nextCursor }; - } - + if (refreshed) { // GraphQL Followers regularly returns 404 (queryId churn / endpoint flakiness). // Fallback to the internal v1.1 REST endpoint used by the web client through the relay. const restAttempt = await this.getFollowersViaRest(userId, count, cursor); if (restAttempt.success) { return restAttempt; } - - return { success: false, error: secondAttempt.error }; } - return { success: false, error: firstAttempt.error }; + return { success: false, error: result.error }; } }