/ clis / twitter / profile.js
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  });