From 769245503c2c4475f1dcf84e9aae959f98f3764c Mon Sep 17 00:00:00 2001 From: Pablo Tovar Date: Mon, 12 Jan 2026 19:47:40 +0100 Subject: [PATCH] feat: add AboutAccountQuery client support --- scripts/update-query-ids.ts | 1 + src/lib/index.ts | 7 +- src/lib/query-ids.json | 3 +- src/lib/twitter-client-constants.ts | 1 + src/lib/twitter-client-types.ts | 14 ++++ src/lib/twitter-client-user-lookup.ts | 93 +++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 2 deletions(-) diff --git a/scripts/update-query-ids.ts b/scripts/update-query-ids.ts index 84227c4..abf32a3 100644 --- a/scripts/update-query-ids.ts +++ b/scripts/update-query-ids.ts @@ -23,6 +23,7 @@ const TARGET_OPERATIONS = [ 'ExplorePage', 'GenericTimelineById', 'TrendHistory', + 'AboutAccountQuery', ] as const; type OperationName = (typeof TARGET_OPERATIONS)[number]; diff --git a/src/lib/index.ts b/src/lib/index.ts index ca52ff5..95608d2 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -14,4 +14,9 @@ export type { ExploreTab, NewsFetchOptions, NewsItem, NewsResult } from './twitt export type { SearchFetchOptions } from './twitter-client-search.js'; export type { TimelineFetchOptions } from './twitter-client-timelines.js'; export type { TweetFetchOptions } from './twitter-client-tweet-detail.js'; -export type { TweetResult, UploadMediaResult } from './twitter-client-types.js'; +export type { + AboutAccountProfile, + AboutAccountResult, + TweetResult, + UploadMediaResult, +} from './twitter-client-types.js'; diff --git a/src/lib/query-ids.json b/src/lib/query-ids.json index 2214567..b75ce7e 100644 --- a/src/lib/query-ids.json +++ b/src/lib/query-ids.json @@ -13,5 +13,6 @@ "ExploreSidebar": "lpSN4M6qpimkF4nRFPE3nQ", "ExplorePage": "kheAINB_4pzRDqkzG3K-ng", "GenericTimelineById": "uGSr7alSjR9v6QJAIaqSKQ", - "TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ" + "TrendHistory": "Sj4T-jSB9pr0Mxtsc1UKZQ", + "AboutAccountQuery": "zs_jFPFT78rBpXv9Z3U2YQ" } diff --git a/src/lib/twitter-client-constants.ts b/src/lib/twitter-client-constants.ts index aa03079..167b830 100644 --- a/src/lib/twitter-client-constants.ts +++ b/src/lib/twitter-client-constants.ts @@ -33,6 +33,7 @@ export const FALLBACK_QUERY_IDS = { ExplorePage: 'kheAINB_4pzRDqkzG3K-ng', GenericTimelineById: 'uGSr7alSjR9v6QJAIaqSKQ', TrendHistory: 'Sj4T-jSB9pr0Mxtsc1UKZQ', + AboutAccountQuery: 'zs_jFPFT78rBpXv9Z3U2YQ', } as const; export type OperationName = keyof typeof FALLBACK_QUERY_IDS; diff --git a/src/lib/twitter-client-types.ts b/src/lib/twitter-client-types.ts index 3f31545..5d176c3 100644 --- a/src/lib/twitter-client-types.ts +++ b/src/lib/twitter-client-types.ts @@ -324,6 +324,20 @@ export interface FollowingResult { nextCursor?: string; } +export interface AboutAccountProfile { + accountBasedIn?: string; + source?: string; + createdCountryAccurate?: boolean; + locationAccurate?: boolean; + learnMoreUrl?: string; +} + +export interface AboutAccountResult { + success: boolean; + aboutProfile?: AboutAccountProfile; + error?: string; +} + export interface TwitterClientOptions { relayBaseUrl?: string; profileName?: string; diff --git a/src/lib/twitter-client-user-lookup.ts b/src/lib/twitter-client-user-lookup.ts index b4d6061..231611b 100644 --- a/src/lib/twitter-client-user-lookup.ts +++ b/src/lib/twitter-client-user-lookup.ts @@ -1,6 +1,7 @@ import { normalizeHandle } from './normalize-handle.js'; import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js'; import { TWITTER_API_BASE } from './twitter-client-constants.js'; +import type { AboutAccountResult } from './twitter-client-types.js'; /** Result of username to userId lookup */ export interface UserLookupResult { @@ -13,6 +14,7 @@ export interface UserLookupResult { export interface TwitterClientUserLookupMethods { getUserIdByUsername(username: string): Promise; + getUserAboutAccount(username: string): Promise; } export function withUserLookup>( @@ -205,6 +207,97 @@ export function withUserLookup { + const primary = await this.getQueryId('AboutAccountQuery'); + return Array.from(new Set([primary, 'zs_jFPFT78rBpXv9Z3U2YQ'])); + } + + /** + * Get account origin and location information for a user. + * Returns data from Twitter's "About this account" feature. + */ + async getUserAboutAccount(username: string): Promise { + const cleanUsername = normalizeHandle(username); + if (!cleanUsername) { + return { success: false, error: `Invalid username: ${username}` }; + } + + const variables = { + screenName: cleanUsername, + }; + + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + }); + + let lastError: string | undefined; + const queryIds = await this.getAboutAccountQueryIds(); + + 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(), + }); + + if (!response.ok) { + const text = await response.text(); + if (response.status === 404) { + lastError = `HTTP ${response.status}`; + 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; + }; + }; + }; + }; + errors?: Array<{ message: string }>; + }; + + 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, + 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, error: lastError ?? 'Unknown error fetching account details' }; + } } return TwitterClientUserLookup;