diff --git a/src/profile.test.ts b/src/profile.test.ts index 3043f3e..b7a876f 100644 --- a/src/profile.test.ts +++ b/src/profile.test.ts @@ -1,6 +1,12 @@ import { Profile } from './profile'; import { getScraper } from './test-utils'; +test('scraper can get screen name by user id', async () => { + const scraper = await getScraper(); + const screenName = await scraper.getScreenNameByUserId('1586562503865008129'); + expect(screenName).toEqual('ligma__sigma'); +}); + test('scraper can get profile', async () => { const expected: Profile = { avatar: diff --git a/src/profile.ts b/src/profile.ts index e36956d..3a29e81 100644 --- a/src/profile.ts +++ b/src/profile.ts @@ -203,6 +203,78 @@ export async function getProfile( const idCache = new Map(); +export async function getScreenNameByUserId( + userId: string, + auth: TwitterAuth, +): Promise> { + const params = new URLSearchParams(); + params.set( + 'variables', + stringify({ + userId: userId, + withSafetyModeUserFields: true, + }), + ); + + params.set( + 'features', + stringify({ + hidden_profile_subscriptions_enabled: true, + rweb_tipjar_consumption_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + verified_phone_label_enabled: false, + highlights_tweets_tab_ui_enabled: true, + responsive_web_twitter_article_notes_tab_enabled: true, + subscriptions_feature_can_gift_premium: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + responsive_web_graphql_timeline_navigation_enabled: true, + }), + ); + + const res = await requestApi( + `https://twitter.com/i/api/graphql/xf3jd90KKBCUxdlI_tNHZw/UserByRestId?${params.toString()}`, + auth, + ); + + if (!res.success) { + return res; + } + + const { value } = res; + const { errors } = value; + if (errors != null && errors.length > 0) { + return { + success: false, + err: new Error(errors[0].message), + }; + } + + if (!value.data || !value.data.user || !value.data.user.result) { + return { + success: false, + err: new Error('User not found.'), + }; + } + + const { result: user } = value.data.user; + const { legacy } = user; + + if (legacy.screen_name == null || legacy.screen_name.length === 0) { + return { + success: false, + err: new Error( + `Either user with ID ${userId} does not exist or is private.`, + ), + }; + } + + return { + success: true, + value: legacy.screen_name, + }; +} + export async function getUserIdByScreenName( screenName: string, auth: TwitterAuth, diff --git a/src/scraper.ts b/src/scraper.ts index ca830b5..641f1bd 100644 --- a/src/scraper.ts +++ b/src/scraper.ts @@ -2,7 +2,12 @@ import { Cookie } from 'tough-cookie'; import { bearerToken, FetchTransformOptions, RequestApiResult } from './api'; import { TwitterAuth, TwitterAuthOptions, TwitterGuestAuth } from './auth'; import { TwitterUserAuth } from './auth-user'; -import { getProfile, getUserIdByScreenName, Profile } from './profile'; +import { + getProfile, + getUserIdByScreenName, + getScreenNameByUserId, + Profile, +} from './profile'; import { fetchSearchProfiles, fetchSearchTweets, @@ -99,6 +104,16 @@ export class Scraper { return this.handleResponse(res); } + /** + * + * @param userId The user ID of the profile to fetch. + * @returns The screen name of the corresponding account. + */ + public async getScreenNameByUserId(userId: string): Promise { + const response = await getScreenNameByUserId(userId, this.auth); + return this.handleResponse(response); + } + /** * Fetches tweets from Twitter. * @param query The search query. Any Twitter-compatible query format can be used. @@ -265,8 +280,8 @@ export class Scraper { /** * Send a tweet - * @param text The text of the tweet - * @returns + * @param text The text of the tweet + * @returns */ async sendTweet(text: string, tweetId?: string) {