refactor: normalize about output + retry helper
This commit is contained in:
+28
-20
@@ -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 {
|
||||
|
||||
@@ -49,6 +49,18 @@ export abstract class TwitterClientBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected async withRefreshedQueryIdsOn404<T extends { success: boolean; had404?: boolean }>(
|
||||
attempt: () => Promise<T>,
|
||||
): 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<string[]> {
|
||||
const primary = await this.getQueryId('TweetDetail');
|
||||
return Array.from(new Set([primary, '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g']));
|
||||
|
||||
@@ -307,21 +307,11 @@ export function withUserLookup<TBase extends AbstractConstructor<TwitterClientBa
|
||||
};
|
||||
};
|
||||
|
||||
const firstAttempt = await tryOnce();
|
||||
if (firstAttempt.success) {
|
||||
return { success: true, aboutProfile: firstAttempt.aboutProfile };
|
||||
const { result } = await this.withRefreshedQueryIdsOn404(tryOnce);
|
||||
if (result.success) {
|
||||
return { success: true, aboutProfile: result.aboutProfile };
|
||||
}
|
||||
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await tryOnce();
|
||||
if (secondAttempt.success) {
|
||||
return { success: true, aboutProfile: secondAttempt.aboutProfile };
|
||||
}
|
||||
return { success: false, error: secondAttempt.error };
|
||||
}
|
||||
|
||||
return { success: false, error: firstAttempt.error };
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -331,29 +331,21 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||
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 };
|
||||
}
|
||||
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await tryOnce();
|
||||
if (secondAttempt.success) {
|
||||
return { success: true, users: secondAttempt.users, nextCursor: secondAttempt.nextCursor };
|
||||
const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce);
|
||||
if (result.success) {
|
||||
return { success: true, users: result.users, nextCursor: result.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<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||
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 };
|
||||
}
|
||||
|
||||
if (firstAttempt.had404) {
|
||||
await this.refreshQueryIds();
|
||||
const secondAttempt = await tryOnce();
|
||||
if (secondAttempt.success) {
|
||||
return { success: true, users: secondAttempt.users, nextCursor: secondAttempt.nextCursor };
|
||||
const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce);
|
||||
if (result.success) {
|
||||
return { success: true, users: result.users, nextCursor: result.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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user