fix: about account live coverage (#51) (thanks @pjtf93)

This commit is contained in:
Peter Steinberger
2026-06-24 18:42:16 +09:00
committed by yuta
parent 058aa0e3c3
commit 77c7d676a7
7 changed files with 97 additions and 54 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ export function createProgram(ctx: CliContext): Command {
].join('\n\n')}\n\n${ctx.colors.section('Shortcuts')}\n${[
formatExample('bird <tweet-id-or-url> [--json]', 'Shorthand for `bird read <tweet-id-or-url>`'),
].join('\n\n')}\n\n${ctx.colors.section('JSON Output')}\n${ctx.colors.muted(
` Add ${ctx.colors.option('--json')} to: read, replies, thread, search, mentions, bookmarks, likes, following, followers, lists, list-timeline, user-tweets, query-ids`,
` Add ${ctx.colors.option('--json')} to: read, replies, thread, search, mentions, bookmarks, likes, following, followers, about, lists, list-timeline, user-tweets, query-ids`,
)}\n${ctx.colors.muted(
` Add ${ctx.colors.option('--json-full')} to include raw API response in ${ctx.colors.argument('_raw')} field (tweet commands only)`,
)}\n${ctx.colors.muted(` (Run ${ctx.colors.command('bird <command> --help')} to see per-command flags.)`)}`,
+6 -1
View File
@@ -1,8 +1,11 @@
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';
const LEADING_AT_REGEX = /^@+/;
type PagedUsersResult = {
success: boolean;
users?: TwitterUser[];
@@ -289,6 +292,7 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
.action(async (username: string, cmdOpts: { json?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const normalizedHandle = normalizeHandle(username);
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
@@ -309,7 +313,8 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
console.log(JSON.stringify(result.aboutProfile, null, 2));
} else {
const profile = result.aboutProfile;
console.log(`${ctx.p('info')}Account information for @${username}:`);
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}`);
}
+76 -51
View File
@@ -231,72 +231,97 @@ export function withUserLookup<TBase extends AbstractConstructor<TwitterClientBa
variables: JSON.stringify(variables),
});
let lastError: string | undefined;
const queryIds = await this.getAboutAccountQueryIds();
const tryOnce = async () => {
let lastError: string | undefined;
let had404 = false;
const queryIds = await this.getAboutAccountQueryIds();
for (const queryId of queryIds) {
const url = `${TWITTER_API_BASE}/${queryId}/AboutAccountQuery?${params.toString()}`;
for (const queryId of queryIds) {
const url = `${TWITTER_API_BASE}/${queryId}/AboutAccountQuery?${params.toString()}`;
try {
const response = await this.fetchWithTimeout(url, {
method: 'GET',
headers: this.getHeaders(),
});
try {
const response = await this.fetchWithTimeout(url, {
method: 'GET',
headers: this.getHeaders(),
});
if (!response.ok) {
const text = await response.text();
if (response.status === 404) {
lastError = `HTTP ${response.status}`;
if (!response.ok) {
const text = await response.text();
if (response.status === 404) {
had404 = true;
lastError = `HTTP ${response.status}`;
continue;
}
lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`;
continue;
}
lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`;
continue;
}
const data = (await response.json()) as {
data?: {
user_result_by_screen_name?: {
result?: {
about_profile?: {
account_based_in?: string;
source?: string;
created_country_accurate?: boolean;
location_accurate?: boolean;
learn_more_url?: string;
const data = (await response.json()) as {
data?: {
user_result_by_screen_name?: {
result?: {
about_profile?: {
account_based_in?: string;
source?: string;
created_country_accurate?: boolean;
location_accurate?: boolean;
learn_more_url?: string;
};
};
};
};
errors?: Array<{ message: string }>;
};
errors?: Array<{ message: string }>;
};
if (data.errors && data.errors.length > 0) {
lastError = data.errors.map((e) => e.message).join(', ');
continue;
if (data.errors && data.errors.length > 0) {
lastError = data.errors.map((e) => e.message).join(', ');
continue;
}
const aboutProfile = data.data?.user_result_by_screen_name?.result?.about_profile;
if (!aboutProfile) {
lastError = 'Missing about_profile in response';
continue;
}
return {
success: true as const,
aboutProfile: {
accountBasedIn: aboutProfile.account_based_in,
source: aboutProfile.source,
createdCountryAccurate: aboutProfile.created_country_accurate,
locationAccurate: aboutProfile.location_accurate,
learnMoreUrl: aboutProfile.learn_more_url,
},
had404,
};
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
const aboutProfile = data.data?.user_result_by_screen_name?.result?.about_profile;
if (!aboutProfile) {
lastError = 'Missing about_profile in response';
continue;
}
return {
success: true,
aboutProfile: {
accountBasedIn: aboutProfile.account_based_in,
source: aboutProfile.source,
createdCountryAccurate: aboutProfile.created_country_accurate,
locationAccurate: aboutProfile.location_accurate,
learnMoreUrl: aboutProfile.learn_more_url,
},
};
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
return {
success: false as const,
error: lastError ?? 'Unknown error fetching account details',
had404,
};
};
const firstAttempt = await tryOnce();
if (firstAttempt.success) {
return { success: true, aboutProfile: firstAttempt.aboutProfile };
}
return { success: false, error: lastError ?? 'Unknown error fetching account details' };
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 };
}
}