profile.js
1 import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors'; 2 import { cli, Strategy } from '@jackwener/opencli/registry'; 3 import { resolveTwitterQueryId } from './shared.js'; 4 const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ'; 5 cli({ 6 site: 'twitter', 7 name: 'profile', 8 description: 'Fetch a Twitter user profile (bio, stats, etc.)', 9 domain: 'x.com', 10 strategy: Strategy.COOKIE, 11 browser: true, 12 args: [ 13 { name: 'username', type: 'string', positional: true, help: 'Twitter screen name (without @). Defaults to logged-in user.' }, 14 ], 15 columns: ['screen_name', 'name', 'bio', 'location', 'url', 'followers', 'following', 'tweets', 'likes', 'verified', 'created_at'], 16 func: async (page, kwargs) => { 17 let username = (kwargs.username || '').replace(/^@/, ''); 18 // If no username, detect the logged-in user 19 if (!username) { 20 await page.goto('https://x.com/home'); 21 await page.wait({ selector: '[data-testid="primaryColumn"]' }); 22 const href = await page.evaluate(`() => { 23 const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]'); 24 return link ? link.getAttribute('href') : null; 25 }`); 26 if (!href) 27 throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?'); 28 username = href.replace('/', ''); 29 } 30 // Navigate directly to the user's profile page (gives us cookie context) 31 await page.goto(`https://x.com/${username}`); 32 await page.wait(3); 33 const queryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID); 34 const result = await page.evaluate(` 35 async () => { 36 const screenName = "${username}"; 37 const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1]; 38 if (!ct0) return {error: 'No ct0 cookie — not logged into x.com'}; 39 40 const bearer = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; 41 const headers = { 42 'Authorization': 'Bearer ' + decodeURIComponent(bearer), 43 'X-Csrf-Token': ct0, 44 'X-Twitter-Auth-Type': 'OAuth2Session', 45 'X-Twitter-Active-User': 'yes' 46 }; 47 48 const variables = JSON.stringify({ 49 screen_name: screenName, 50 withSafetyModeUserFields: true, 51 }); 52 const features = JSON.stringify({ 53 hidden_profile_subscriptions_enabled: true, 54 rweb_tipjar_consumption_enabled: true, 55 responsive_web_graphql_exclude_directive_enabled: true, 56 verified_phone_label_enabled: false, 57 subscriptions_verification_info_is_identity_verified_enabled: true, 58 subscriptions_verification_info_verified_since_enabled: true, 59 highlights_tweets_tab_ui_enabled: true, 60 responsive_web_twitter_article_notes_tab_enabled: true, 61 subscriptions_feature_can_gift_premium: true, 62 creator_subscriptions_tweet_preview_api_enabled: true, 63 responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, 64 responsive_web_graphql_timeline_navigation_enabled: true, 65 }); 66 67 const url = '/i/api/graphql/' + ${JSON.stringify(queryId)} + '/UserByScreenName?variables=' 68 + encodeURIComponent(variables) 69 + '&features=' + encodeURIComponent(features); 70 71 const resp = await fetch(url, {headers, credentials: 'include'}); 72 if (!resp.ok) return {error: 'HTTP ' + resp.status, hint: 'User may not exist or queryId expired'}; 73 const d = await resp.json(); 74 75 const result = d.data?.user?.result; 76 if (!result) return {error: 'User @' + screenName + ' not found'}; 77 78 const legacy = result.legacy || {}; 79 const expandedUrl = legacy.entities?.url?.urls?.[0]?.expanded_url || ''; 80 81 return [{ 82 screen_name: legacy.screen_name || screenName, 83 name: legacy.name || '', 84 bio: legacy.description || '', 85 location: legacy.location || '', 86 url: expandedUrl, 87 followers: legacy.followers_count || 0, 88 following: legacy.friends_count || 0, 89 tweets: legacy.statuses_count || 0, 90 likes: legacy.favourites_count || 0, 91 verified: result.is_blue_verified || legacy.verified || false, 92 created_at: legacy.created_at || '', 93 }]; 94 } 95 `); 96 if (result?.error) { 97 if (String(result.error).includes('No ct0 cookie')) 98 throw new AuthRequiredError('x.com', result.error); 99 throw new CommandExecutionError(result.error + (result.hint ? ` (${result.hint})` : '')); 100 } 101 return result || []; 102 } 103 });