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 type { CliContext } from '../cli/shared.js';
|
||||||
import { normalizeHandle } from '../lib/normalize-handle.js';
|
import { normalizeHandle } from '../lib/normalize-handle.js';
|
||||||
import { TwitterClient } from '../lib/twitter-client.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 = {
|
type PagedUsersResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -294,6 +312,11 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||||
const normalizedHandle = normalizeHandle(username);
|
const normalizedHandle = normalizeHandle(username);
|
||||||
|
|
||||||
|
if (!normalizedHandle) {
|
||||||
|
console.error(`${ctx.p('err')}Invalid username: ${username}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||||
|
|
||||||
for (const warning of warnings) {
|
for (const warning of warnings) {
|
||||||
@@ -306,29 +329,14 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies, timeoutMs });
|
const client = new TwitterClient({ cookies, timeoutMs });
|
||||||
const result = await client.getUserAboutAccount(username);
|
const result = await client.getUserAboutAccount(normalizedHandle);
|
||||||
|
|
||||||
if (result.success && result.aboutProfile) {
|
if (result.success && result.aboutProfile) {
|
||||||
if (cmdOpts.json) {
|
if (cmdOpts.json) {
|
||||||
console.log(JSON.stringify(result.aboutProfile, null, 2));
|
console.log(JSON.stringify(result.aboutProfile, null, 2));
|
||||||
} else {
|
} else {
|
||||||
const profile = result.aboutProfile;
|
for (const line of formatAboutProfile(result.aboutProfile, ctx, normalizedHandle)) {
|
||||||
const displayHandle = normalizedHandle ?? username.replace(LEADING_AT_REGEX, '');
|
console.log(line);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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[]> {
|
protected async getTweetDetailQueryIds(): Promise<string[]> {
|
||||||
const primary = await this.getQueryId('TweetDetail');
|
const primary = await this.getQueryId('TweetDetail');
|
||||||
return Array.from(new Set([primary, '97JF30KziU00483E_8elBA', 'aFvUsJm2c-oDkJV75blV6g']));
|
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();
|
const { result } = await this.withRefreshedQueryIdsOn404(tryOnce);
|
||||||
if (firstAttempt.success) {
|
if (result.success) {
|
||||||
return { success: true, aboutProfile: firstAttempt.aboutProfile };
|
return { success: true, aboutProfile: result.aboutProfile };
|
||||||
}
|
}
|
||||||
|
return { success: false, error: result.error };
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -331,29 +331,21 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
|
|||||||
return { success: false as const, error: lastError ?? 'Unknown error fetching following', had404 };
|
return { success: false as const, error: lastError ?? 'Unknown error fetching following', had404 };
|
||||||
};
|
};
|
||||||
|
|
||||||
const firstAttempt = await tryOnce();
|
const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce);
|
||||||
if (firstAttempt.success) {
|
if (result.success) {
|
||||||
return { success: true, users: firstAttempt.users, nextCursor: firstAttempt.nextCursor };
|
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).
|
// 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.
|
// Fallback to the internal v1.1 REST endpoint used by the web client through the relay.
|
||||||
const restAttempt = await this.getFollowingViaRest(userId, count, cursor);
|
const restAttempt = await this.getFollowingViaRest(userId, count, cursor);
|
||||||
if (restAttempt.success) {
|
if (restAttempt.success) {
|
||||||
return restAttempt;
|
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 };
|
return { success: false as const, error: lastError ?? 'Unknown error fetching followers', had404 };
|
||||||
};
|
};
|
||||||
|
|
||||||
const firstAttempt = await tryOnce();
|
const { result, refreshed } = await this.withRefreshedQueryIdsOn404(tryOnce);
|
||||||
if (firstAttempt.success) {
|
if (result.success) {
|
||||||
return { success: true, users: firstAttempt.users, nextCursor: firstAttempt.nextCursor };
|
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).
|
// 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.
|
// Fallback to the internal v1.1 REST endpoint used by the web client through the relay.
|
||||||
const restAttempt = await this.getFollowersViaRest(userId, count, cursor);
|
const restAttempt = await this.getFollowersViaRest(userId, count, cursor);
|
||||||
if (restAttempt.success) {
|
if (restAttempt.success) {
|
||||||
return restAttempt;
|
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