import { normalizeHandle } from '../lib/normalize-handle.js'; import { TwitterClient } from '../lib/twitter-client.js'; function formatAboutProfile(profile, ctx, handle) { const lines = [`${ctx.p('info')}Account information for @${handle}:`]; if (profile.accountBasedIn) { lines.push(` Account based in: ${profile.accountBasedIn}`); } if (profile.createdCountryAccurate !== undefined) { lines.push(` Creation country accurate: ${profile.createdCountryAccurate ? 'Yes' : 'No'}`); } if (profile.locationAccurate !== undefined) { lines.push(` Location accurate: ${profile.locationAccurate ? 'Yes' : 'No'}`); } if (profile.source) { lines.push(`${ctx.l('source')}${profile.source}`); } if (profile.learnMoreUrl) { lines.push(` Learn more: ${profile.learnMoreUrl}`); } return lines; } function printUsers(users, ctx) { for (const user of users) { console.log(`@${user.username} (${user.name})`); if (user.description) { console.log(` ${user.description.slice(0, 100)}${user.description.length > 100 ? '...' : ''}`); } if (user.followersCount !== undefined) { console.log(` ${ctx.p('info')}${user.followersCount.toLocaleString()} followers`); } console.log('──────────────────────────────────────────────────'); } } async function resolveUserIdOrExit(client, requestedUserId, ctx) { if (requestedUserId) { return requestedUserId; } const currentUser = await client.getCurrentUser(); if (!currentUser.success || !currentUser.user?.id) { console.error(`${ctx.p('err')}Failed to get current user: ${currentUser.error || 'Unknown error'}`); process.exit(1); } return currentUser.user.id; } async function runUserListCommand(program, ctx, spec, cmdOpts) { const opts = program.opts(); const timeoutMs = ctx.resolveTimeoutFromOptions(opts); const count = Number.parseInt(cmdOpts.count || '20', 10); const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined; const usePagination = cmdOpts.all || cmdOpts.cursor; if (maxPages !== undefined && !cmdOpts.all) { console.error(`${ctx.p('err')}--max-pages requires --all.`); process.exit(1); } if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) { console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`); process.exit(1); } const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); for (const warning of warnings) { console.error(`${ctx.p('warn')}${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error(`${ctx.p('err')}Missing required credentials`); process.exit(1); } const client = new TwitterClient({ cookies, timeoutMs }); const userId = await resolveUserIdOrExit(client, cmdOpts.user, ctx); if (cmdOpts.all) { const allUsers = []; const seen = new Set(); let cursor = cmdOpts.cursor; let pageNum = 0; let nextCursor; while (true) { pageNum += 1; if (!cmdOpts.json) { console.error(`${ctx.p('info')}Fetching page ${pageNum}...`); } const result = await spec.fetch(client, userId, count, cursor); if (!result.success || !result.users) { console.error(`${ctx.p('err')}Failed to fetch ${spec.name}: ${result.error}`); process.exit(1); } let added = 0; for (const user of result.users) { if (!seen.has(user.id)) { seen.add(user.id); allUsers.push(user); added += 1; } } const pageCursor = result.nextCursor; if (!pageCursor || result.users.length === 0 || added === 0 || pageCursor === cursor) { nextCursor = undefined; break; } if (maxPages && pageNum >= maxPages) { nextCursor = pageCursor; break; } cursor = pageCursor; await new Promise((resolve) => setTimeout(resolve, 1000)); } if (cmdOpts.json) { console.log(JSON.stringify({ users: allUsers, nextCursor: nextCursor ?? null }, null, 2)); } else { console.error(`${ctx.p('info')}Total: ${allUsers.length} users`); if (nextCursor) { console.error(`${ctx.p('info')}Stopped at --max-pages. Use --cursor to continue.`); console.error(`${ctx.p('info')}Next cursor: ${nextCursor}`); } printUsers(allUsers, ctx); } return; } const result = await spec.fetch(client, userId, count, cmdOpts.cursor); if (result.success && result.users) { if (cmdOpts.json) { if (usePagination) { console.log(JSON.stringify({ users: result.users, nextCursor: result.nextCursor ?? null }, null, 2)); } else { console.log(JSON.stringify(result.users, null, 2)); } } else { if (result.users.length === 0) { console.log('No users found.'); } else { printUsers(result.users, ctx); if (result.nextCursor) { console.error(`${ctx.p('info')}Next cursor: ${result.nextCursor}`); } } } } else { console.error(`${ctx.p('err')}Failed to fetch ${spec.name}: ${result.error}`); process.exit(1); } } export function registerUserCommands(program, ctx) { const registerUserListCommand = (spec) => { program .command(spec.name) .description(spec.description) .option('--user ', `User ID to get ${spec.name} for (defaults to current user)`) .option('-n, --count ', 'Number of users to fetch per page', '20') .option('--cursor ', 'Cursor for pagination (from previous response)') .option('--all', 'Fetch all users (paginate automatically)') .option('--max-pages ', 'Stop after N pages when using --all') .option('--json', 'Output as JSON') .action(async (cmdOpts) => runUserListCommand(program, ctx, spec, cmdOpts)); }; registerUserListCommand({ name: 'following', description: 'Get users that you (or another user) follow', fetch: (client, userId, count, cursor) => client.getFollowing(userId, count, cursor), }); registerUserListCommand({ name: 'followers', description: 'Get users that follow you (or another user)', fetch: (client, userId, count, cursor) => client.getFollowers(userId, count, cursor), }); program .command('likes') .description('Get your liked tweets') .option('-n, --count ', 'Number of likes to fetch', '20') .option('--all', 'Fetch all likes (paged)') .option('--max-pages ', 'Stop after N pages when using --all') .option('--cursor ', 'Resume pagination from a cursor') .option('--json', 'Output as JSON') .option('--json-full', 'Output as JSON with full raw API response in _raw field') .action(async (cmdOpts) => { const opts = program.opts(); const timeoutMs = ctx.resolveTimeoutFromOptions(opts); const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); const count = Number.parseInt(cmdOpts.count || '20', 10); const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined; const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); for (const warning of warnings) { console.error(`${ctx.p('warn')}${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error(`${ctx.p('err')}Missing required credentials`); process.exit(1); } const usePagination = cmdOpts.all || cmdOpts.cursor; if (maxPages !== undefined && !usePagination) { console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`); process.exit(1); } if (!usePagination && (!Number.isFinite(count) || count <= 0)) { console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); process.exit(1); } if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) { console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`); process.exit(1); } const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const includeRaw = cmdOpts.jsonFull ?? false; const timelineOptions = { includeRaw }; const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor }; const result = usePagination ? await client.getAllLikes(paginationOptions) : await client.getLikes(count, timelineOptions); if (result.success) { const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); ctx.printTweetsResult(result, { json: isJson, usePagination: Boolean(usePagination), emptyMessage: 'No liked tweets found.', }); } else { console.error(`${ctx.p('err')}Failed to fetch likes: ${result.error}`); process.exit(1); } }); program .command('whoami') .description('Show which Twitter account the current credentials belong to') .action(async () => { const opts = program.opts(); const timeoutMs = ctx.resolveTimeoutFromOptions(opts); const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); for (const warning of warnings) { console.error(`${ctx.p('warn')}${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error(`${ctx.p('err')}Missing required credentials`); process.exit(1); } if (cookies.source) { console.error(`${ctx.l('source')}${cookies.source}`); } const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const result = await client.getCurrentUser(); const credentialSource = cookies.source ?? 'env/auto-detected cookies'; if (result.success && result.user) { console.log(`${ctx.l('user')}@${result.user.username} (${result.user.name})`); console.log(`${ctx.l('userId')}${result.user.id}`); console.log(`${ctx.l('engine')}graphql`); console.log(`${ctx.l('credentials')}${credentialSource}`); } else { console.error(`${ctx.p('err')}Failed to determine current user: ${result.error ?? 'Unknown error'}`); process.exit(1); } }); program .command('about') .description('Get account origin and location information for a user') .argument('', 'Twitter username (with or without @)') .option('--json', 'Output as JSON') .action(async (username, cmdOpts) => { const opts = program.opts(); const timeoutMs = ctx.resolveTimeoutFromOptions(opts); const normalizedHandle = normalizeHandle(username); if (!normalizedHandle) { console.error(`${ctx.p('err')}Invalid username: ${username}`); process.exit(1); } const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); for (const warning of warnings) { console.error(`${ctx.p('warn')}${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error(`${ctx.p('err')}Missing required credentials`); process.exit(1); } const client = new TwitterClient({ cookies, timeoutMs }); const result = await client.getUserAboutAccount(normalizedHandle); if (result.success && result.aboutProfile) { if (cmdOpts.json) { console.log(JSON.stringify(result.aboutProfile, null, 2)); } else { for (const line of formatAboutProfile(result.aboutProfile, ctx, normalizedHandle)) { console.log(line); } } } else { console.error(`${ctx.p('err')}Failed to fetch account information: ${result.error ?? 'Unknown error'}`); process.exit(1); } }); } //# sourceMappingURL=users.js.map