import { normalizeHandle } from './normalize-handle.js'; import { TWITTER_API_BASE } from './twitter-client-constants.js'; export function withUserLookup(Base) { class TwitterClientUserLookup extends Base { // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. constructor(...args) { super(...args); } async getUserByScreenNameGraphQL(screenName) { // UserByScreenName query IDs observed from web client const queryIds = ['xc8f1g7BYqr6VTzTbvNlGw', 'qW5u-DAuXpMEG0zA1F7UGQ', 'sLVLhk0bGj3MVFEKTdax1w']; const variables = { screen_name: screenName, withSafetyModeUserFields: true, }; const features = { hidden_profile_subscriptions_enabled: true, hidden_profile_likes_enabled: true, rweb_tipjar_consumption_enabled: true, responsive_web_graphql_exclude_directive_enabled: true, verified_phone_label_enabled: false, subscriptions_verification_info_is_identity_verified_enabled: true, subscriptions_verification_info_verified_since_enabled: true, highlights_tweets_tab_ui_enabled: true, responsive_web_twitter_article_notes_tab_enabled: true, subscriptions_feature_can_gift_premium: true, creator_subscriptions_tweet_preview_api_enabled: true, responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, responsive_web_graphql_timeline_navigation_enabled: true, blue_business_profile_image_shape_enabled: true, }; const fieldToggles = { withAuxiliaryUserLabels: false, }; const params = new URLSearchParams({ variables: JSON.stringify(variables), features: JSON.stringify(features), fieldToggles: JSON.stringify(fieldToggles), }); let lastError; for (const queryId of queryIds) { const url = `${TWITTER_API_BASE}/${queryId}/UserByScreenName?${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) { // Try next query ID lastError = `HTTP ${response.status}`; continue; } lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; continue; } const data = (await response.json()); // Check for user not found if (data.data?.user?.result?.__typename === 'UserUnavailable') { return { success: false, error: `User @${screenName} not found or unavailable` }; } const userResult = data.data?.user?.result; const userId = userResult?.rest_id; const username = userResult?.legacy?.screen_name ?? userResult?.core?.screen_name; const name = userResult?.legacy?.name ?? userResult?.core?.name; if (userId && username) { return { success: true, userId, username, name, }; } if (data.errors && data.errors.length > 0) { lastError = data.errors.map((e) => e.message).join(', '); continue; } lastError = 'Could not parse user data from response'; } catch (error) { lastError = error instanceof Error ? error.message : String(error); } } return { success: false, error: lastError ?? 'Unknown error looking up user' }; } /** * Look up a user's ID by their username/handle. * Uses GraphQL UserByScreenName first, then falls back to REST on transient failures. */ async getUserIdByUsername(username) { const cleanUsername = normalizeHandle(username); if (!cleanUsername) { return { success: false, error: `Invalid username: ${username}` }; } const graphqlResult = await this.getUserByScreenNameGraphQL(cleanUsername); if (graphqlResult.success) { return graphqlResult; } // If GraphQL definitively says user is unavailable, don't retry with REST if (graphqlResult.error?.includes('not found or unavailable')) { return graphqlResult; } // Fallback to REST API for transient GraphQL errors const urls = [ `https://x.com/i/api/1.1/users/show.json?screen_name=${encodeURIComponent(cleanUsername)}`, `https://api.twitter.com/1.1/users/show.json?screen_name=${encodeURIComponent(cleanUsername)}`, ]; let lastError = graphqlResult.error; for (const url of urls) { try { const response = await this.fetchWithTimeout(url, { method: 'GET', headers: this.getHeaders(), }); if (!response.ok) { const text = await response.text(); if (response.status === 404) { return { success: false, error: `User @${cleanUsername} not found` }; } lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; continue; } const data = (await response.json()); const userId = data.id_str ?? (data.id ? String(data.id) : null); if (!userId) { lastError = 'Could not parse user ID from response'; continue; } return { success: true, userId, username: data.screen_name ?? cleanUsername, name: data.name, }; } catch (error) { lastError = error instanceof Error ? error.message : String(error); } } return { success: false, error: lastError ?? 'Unknown error looking up user' }; } async getAboutAccountQueryIds() { 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) { 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), }); const tryOnce = async () => { let lastError; let had404 = false; 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) { had404 = true; lastError = `HTTP ${response.status}`; continue; } lastError = `HTTP ${response.status}: ${text.slice(0, 200)}`; continue; } const data = (await response.json()); 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, }, had404, }; } catch (error) { lastError = error instanceof Error ? error.message : String(error); } } return { success: false, error: lastError ?? 'Unknown error fetching account details', had404, }; }; const { result } = await this.withRefreshedQueryIdsOn404(tryOnce); if (result.success) { return { success: true, aboutProfile: result.aboutProfile }; } return { success: false, error: result.error }; } } return TwitterClientUserLookup; } //# sourceMappingURL=twitter-client-user-lookup.js.map