203 lines
5.5 KiB
JavaScript
203 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* bird - CLI tool for posting tweets and replies
|
|
*
|
|
* Usage:
|
|
* bird tweet "Hello world!"
|
|
* bird reply <tweet-id> "This is a reply"
|
|
* bird reply <tweet-url> "This is a reply"
|
|
* bird read <tweet-id-or-url>
|
|
*/
|
|
|
|
import { Command } from 'commander';
|
|
import { resolveCredentials } from './lib/cookies.js';
|
|
import { extractTweetId } from './lib/extract-tweet-id.js';
|
|
import { TwitterClient } from './lib/twitter-client.js';
|
|
|
|
const program = new Command();
|
|
|
|
program.name('bird').description('Post tweets and replies via Twitter/X GraphQL API').version('0.1.0');
|
|
|
|
// Global options for authentication
|
|
program
|
|
.option('--auth-token <token>', 'Twitter auth_token cookie')
|
|
.option('--ct0 <token>', 'Twitter ct0 cookie')
|
|
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction');
|
|
|
|
// Tweet command
|
|
program
|
|
.command('tweet')
|
|
.description('Post a new tweet')
|
|
.argument('<text>', 'Tweet text')
|
|
.action(async (text: string) => {
|
|
const opts = program.opts();
|
|
const { cookies, warnings } = await resolveCredentials({
|
|
authToken: opts.authToken,
|
|
ct0: opts.ct0,
|
|
chromeProfile: opts.chromeProfile,
|
|
});
|
|
|
|
for (const warning of warnings) {
|
|
console.error(`⚠️ ${warning}`);
|
|
}
|
|
|
|
if (!cookies.authToken || !cookies.ct0) {
|
|
console.error('❌ Missing required credentials');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (cookies.source) {
|
|
console.error(`📍 Using credentials from: ${cookies.source}`);
|
|
}
|
|
|
|
const client = new TwitterClient({ cookies });
|
|
const result = await client.tweet(text);
|
|
|
|
if (result.success) {
|
|
console.log('✅ Tweet posted successfully!');
|
|
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
|
|
} else {
|
|
console.error(`❌ Failed to post tweet: ${result.error}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// Reply command
|
|
program
|
|
.command('reply')
|
|
.description('Reply to an existing tweet')
|
|
.argument('<tweet-id-or-url>', 'Tweet ID or URL to reply to')
|
|
.argument('<text>', 'Reply text')
|
|
.action(async (tweetIdOrUrl: string, text: string) => {
|
|
const opts = program.opts();
|
|
const { cookies, warnings } = await resolveCredentials({
|
|
authToken: opts.authToken,
|
|
ct0: opts.ct0,
|
|
chromeProfile: opts.chromeProfile,
|
|
});
|
|
|
|
for (const warning of warnings) {
|
|
console.error(`⚠️ ${warning}`);
|
|
}
|
|
|
|
if (!cookies.authToken || !cookies.ct0) {
|
|
console.error('❌ Missing required credentials');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (cookies.source) {
|
|
console.error(`📍 Using credentials from: ${cookies.source}`);
|
|
}
|
|
|
|
const tweetId = extractTweetId(tweetIdOrUrl);
|
|
console.error(`📝 Replying to tweet: ${tweetId}`);
|
|
|
|
const client = new TwitterClient({ cookies });
|
|
const result = await client.reply(text, tweetId);
|
|
|
|
if (result.success) {
|
|
console.log('✅ Reply posted successfully!');
|
|
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
|
|
} else {
|
|
console.error(`❌ Failed to post reply: ${result.error}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// Read command - fetch tweet content
|
|
program
|
|
.command('read')
|
|
.description('Read/fetch a tweet by ID or URL')
|
|
.argument('<tweet-id-or-url>', 'Tweet ID or URL to read')
|
|
.option('--json', 'Output as JSON')
|
|
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
|
|
const opts = program.opts();
|
|
const { cookies, warnings } = await resolveCredentials({
|
|
authToken: opts.authToken,
|
|
ct0: opts.ct0,
|
|
chromeProfile: opts.chromeProfile,
|
|
});
|
|
|
|
for (const warning of warnings) {
|
|
console.error(`⚠️ ${warning}`);
|
|
}
|
|
|
|
if (!cookies.authToken || !cookies.ct0) {
|
|
console.error('❌ Missing required credentials');
|
|
process.exit(1);
|
|
}
|
|
|
|
const tweetId = extractTweetId(tweetIdOrUrl);
|
|
const client = new TwitterClient({ cookies });
|
|
const result = await client.getTweet(tweetId);
|
|
|
|
if (result.success && result.tweet) {
|
|
if (cmdOpts.json) {
|
|
console.log(JSON.stringify(result.tweet, null, 2));
|
|
} else {
|
|
console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`);
|
|
console.log(result.tweet.text);
|
|
if (result.tweet.createdAt) {
|
|
console.log(`\n📅 ${result.tweet.createdAt}`);
|
|
}
|
|
console.log(
|
|
`❤️ ${result.tweet.likeCount ?? 0} 🔁 ${result.tweet.retweetCount ?? 0} 💬 ${result.tweet.replyCount ?? 0}`,
|
|
);
|
|
}
|
|
} else {
|
|
console.error(`❌ Failed to read tweet: ${result.error}`);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// Check command - verify credentials
|
|
program
|
|
.command('check')
|
|
.description('Check credential availability')
|
|
.action(async () => {
|
|
const opts = program.opts();
|
|
const { cookies, warnings } = await resolveCredentials({
|
|
authToken: opts.authToken,
|
|
ct0: opts.ct0,
|
|
chromeProfile: opts.chromeProfile,
|
|
});
|
|
|
|
console.log('🔍 Credential Check');
|
|
console.log('─'.repeat(40));
|
|
|
|
if (cookies.authToken) {
|
|
console.log(`✅ auth_token: ${cookies.authToken.slice(0, 10)}...`);
|
|
} else {
|
|
console.log('❌ auth_token: not found');
|
|
}
|
|
|
|
if (cookies.ct0) {
|
|
console.log(`✅ ct0: ${cookies.ct0.slice(0, 10)}...`);
|
|
} else {
|
|
console.log('❌ ct0: not found');
|
|
}
|
|
|
|
if (cookies.source) {
|
|
console.log(`📍 Source: ${cookies.source}`);
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
console.log('\n⚠️ Warnings:');
|
|
for (const warning of warnings) {
|
|
console.log(` - ${warning}`);
|
|
}
|
|
}
|
|
|
|
if (cookies.authToken && cookies.ct0) {
|
|
console.log('\n✅ Ready to tweet!');
|
|
} else {
|
|
console.log('\n❌ Missing credentials. Options:');
|
|
console.log(' 1. Login to x.com in Chrome');
|
|
console.log(' 2. Set AUTH_TOKEN and CT0 environment variables');
|
|
console.log(' 3. Use --auth-token and --ct0 flags');
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
program.parse();
|