feat: default to graphql+firefox, fallback to sweetistics
This commit is contained in:
@@ -42,7 +42,7 @@ bird replies https://x.com/user/status/1234567890123456789
|
||||
```
|
||||
|
||||
Transport (engine) selection:
|
||||
- `--engine graphql|sweetistics|auto` (default `auto`).
|
||||
- `--engine graphql|sweetistics|auto` (default `graphql`).
|
||||
- `sweetistics`: use Sweetistics API key, no browser cookies needed.
|
||||
- `graphql`: use Twitter/X GraphQL with cookies (Chrome/Firefox/env/flags).
|
||||
- `auto`: Sweetistics if an API key is available, otherwise GraphQL.
|
||||
@@ -52,31 +52,28 @@ You can set persistent defaults via config files (JSON5):
|
||||
- Global: `~/.config/bird/config.json5`
|
||||
- Project: `./.birdrc.json5` (overrides global)
|
||||
|
||||
Example `~/.config/bird/config.json5`:
|
||||
Example `~/.config/bird/config.json5` (Firefox + GraphQL defaults):
|
||||
|
||||
```json5
|
||||
{
|
||||
// Default to Sweetistics unless overridden by --engine or BIRD_ENGINE
|
||||
engine: "sweetistics",
|
||||
engine: "graphql",
|
||||
// Prefer Firefox cookies by default
|
||||
firefoxProfile: "default-release",
|
||||
// Optional: Sweetistics defaults
|
||||
sweetisticsApiKey: "sweet-...",
|
||||
sweetisticsBaseUrl: "https://sweetistics.com"
|
||||
// Optional: Sweetistics defaults if you want fallback/overrides
|
||||
sweetisticsApiKey: "sweet-..."
|
||||
}
|
||||
```
|
||||
|
||||
Precedence: CLI flags > environment variables > project config > global config.
|
||||
|
||||
To default to Firefox and Sweetistics, create `~/.config/bird/config.json5`:
|
||||
### Credential sources (macOS)
|
||||
|
||||
```json5
|
||||
{
|
||||
engine: "sweetistics",
|
||||
firefoxProfile: "default-release",
|
||||
sweetisticsApiKey: "sweet-..."
|
||||
}
|
||||
```
|
||||
Used only when transport resolves to **graphql**:
|
||||
- **Firefox (default)**: `~/Library/Application Support/Firefox/Profiles/<profile>/cookies.sqlite`. `--firefox-profile <name>` (defaults to `default-release` if present).
|
||||
- **Chrome**: `~/Library/Application Support/Google/Chrome/<Profile>/Cookies` (WAL/SHM copied too). `--chrome-profile <name>`.
|
||||
- **Env/flags** always override browser cookies.
|
||||
|
||||
Transport is chosen first; then, if transport is GraphQL, the cookie source is resolved with the same precedence.
|
||||
|
||||
### Post a tweet
|
||||
|
||||
|
||||
+132
-10
@@ -98,7 +98,11 @@ 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', config.chromeProfile)
|
||||
.option('--firefox-profile <name>', 'Firefox profile name for cookie extraction', config.firefoxProfile)
|
||||
.option(
|
||||
'--firefox-profile <name>',
|
||||
'Firefox profile name for cookie extraction',
|
||||
config.firefoxProfile || 'default-release',
|
||||
)
|
||||
.option('--sweetistics-api-key <key>', 'Sweetistics API key (or set SWEETISTICS_API_KEY)')
|
||||
.option(
|
||||
'--sweetistics-base-url <url>',
|
||||
@@ -108,7 +112,7 @@ program
|
||||
.option(
|
||||
'--engine <engine>',
|
||||
'Engine: graphql | sweetistics | auto',
|
||||
process.env.BIRD_ENGINE || config.engine || 'auto',
|
||||
process.env.BIRD_ENGINE || config.engine || 'graphql',
|
||||
);
|
||||
|
||||
type EngineMode = 'graphql' | 'sweetistics' | 'auto';
|
||||
@@ -227,6 +231,21 @@ program
|
||||
if (result.success) {
|
||||
console.log('✅ Tweet posted successfully!');
|
||||
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL tweet failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({
|
||||
baseUrl: sweetistics.baseUrl,
|
||||
apiKey: sweetistics.apiKey,
|
||||
}).tweet(text);
|
||||
if (fallback.success) {
|
||||
console.log('✅ Tweet posted via Sweetistics (fallback)!');
|
||||
if (fallback.tweetId) {
|
||||
console.log(`🔗 https://x.com/i/status/${fallback.tweetId}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to post tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to post tweet: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -303,6 +322,21 @@ program
|
||||
if (result.success) {
|
||||
console.log('✅ Reply posted successfully!');
|
||||
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL reply failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({
|
||||
baseUrl: sweetistics.baseUrl,
|
||||
apiKey: sweetistics.apiKey,
|
||||
}).tweet(text, tweetId);
|
||||
if (fallback.success) {
|
||||
console.log('✅ Reply posted via Sweetistics (fallback)!');
|
||||
if (fallback.tweetId) {
|
||||
console.log(`🔗 https://x.com/i/status/${fallback.tweetId}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to post reply: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to post reply: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -383,6 +417,28 @@ program
|
||||
`❤️ ${result.tweet.likeCount ?? 0} 🔁 ${result.tweet.retweetCount ?? 0} 💬 ${result.tweet.replyCount ?? 0}`,
|
||||
);
|
||||
}
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL read failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).read(
|
||||
tweetId,
|
||||
);
|
||||
if (fallback.success && fallback.tweet) {
|
||||
if (cmdOpts.json) {
|
||||
console.log(JSON.stringify(fallback.tweet, null, 2));
|
||||
} else {
|
||||
console.log(`@${fallback.tweet.author.username} (${fallback.tweet.author.name}):`);
|
||||
console.log(fallback.tweet.text);
|
||||
if (fallback.tweet.createdAt) {
|
||||
console.log(`\n📅 ${fallback.tweet.createdAt}`);
|
||||
}
|
||||
console.log(
|
||||
`❤️ ${fallback.tweet.likeCount ?? 0} 🔁 ${fallback.tweet.retweetCount ?? 0} 💬 ${fallback.tweet.replyCount ?? 0}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to read tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to read tweet: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -440,6 +496,18 @@ program
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL replies failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({
|
||||
baseUrl: sweetistics.baseUrl,
|
||||
apiKey: sweetistics.apiKey,
|
||||
}).replies(tweetId);
|
||||
if (fallback.success && fallback.tweets) {
|
||||
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
|
||||
} else {
|
||||
console.error(`❌ Failed to fetch replies: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to fetch replies: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -497,6 +565,17 @@ program
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL thread failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).thread(
|
||||
tweetId,
|
||||
);
|
||||
if (fallback.success && fallback.tweets) {
|
||||
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
|
||||
} else {
|
||||
console.error(`❌ Failed to fetch thread: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to fetch thread: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -556,6 +635,18 @@ program
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL search failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).search(
|
||||
query,
|
||||
count,
|
||||
);
|
||||
if (fallback.success && fallback.tweets) {
|
||||
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
|
||||
} else {
|
||||
console.error(`❌ Search failed: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Search failed: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -610,6 +701,18 @@ program
|
||||
|
||||
if (result.success && result.tweets) {
|
||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
|
||||
} else if (sweetistics.apiKey) {
|
||||
console.error(`⚠️ GraphQL mentions failed (${result.error}); trying Sweetistics fallback...`);
|
||||
const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).search(
|
||||
'@clawdbot',
|
||||
count,
|
||||
);
|
||||
if (fallback.success && fallback.tweets) {
|
||||
printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
|
||||
} else {
|
||||
console.error(`❌ Failed to fetch mentions: ${result.error} | Sweetistics fallback: ${fallback.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ Failed to fetch mentions: ${result.error}`);
|
||||
process.exit(1);
|
||||
@@ -635,14 +738,14 @@ program
|
||||
? 'Sweetistics API key'
|
||||
: 'none'
|
||||
: opts.chromeProfile
|
||||
? `Chrome profile "${opts.chromeProfile}"`
|
||||
: opts.firefoxProfile
|
||||
? `Firefox profile "${opts.firefoxProfile}"`
|
||||
: config.chromeProfile
|
||||
? `Chrome profile "${config.chromeProfile}"`
|
||||
: config.firefoxProfile
|
||||
? `Firefox profile "${config.firefoxProfile}"`
|
||||
: 'env/auto-detected cookies';
|
||||
? `Chrome profile "${opts.chromeProfile}"`
|
||||
: opts.firefoxProfile
|
||||
? `Firefox profile "${opts.firefoxProfile}"`
|
||||
: config.chromeProfile
|
||||
? `Chrome profile "${config.chromeProfile}"`
|
||||
: config.firefoxProfile
|
||||
? `Firefox profile "${config.firefoxProfile}"`
|
||||
: 'env/auto-detected cookies';
|
||||
|
||||
if (useSweetistics) {
|
||||
if (!sweetistics.apiKey) {
|
||||
@@ -699,6 +802,25 @@ program
|
||||
console.log(`⚙️ Engine: ${resolvedEngine}`);
|
||||
console.log(`🔑 Credentials: ${credentialSource}`);
|
||||
} else {
|
||||
// Fallback: try Sweetistics if available
|
||||
if (sweetistics.apiKey) {
|
||||
const fallback = await new SweetisticsClient({
|
||||
baseUrl: sweetistics.baseUrl,
|
||||
apiKey: sweetistics.apiKey,
|
||||
}).getCurrentUser();
|
||||
if (fallback.success && fallback.user) {
|
||||
const handle = fallback.user.username ? `@${fallback.user.username}` : '(no handle)';
|
||||
const name = fallback.user.name || handle;
|
||||
console.log(`🙋 Logged in via Sweetistics as ${handle} (${name})`);
|
||||
console.log(`🪪 User ID: ${fallback.user.id}`);
|
||||
if (fallback.user.email) {
|
||||
console.log(`📧 ${fallback.user.email}`);
|
||||
}
|
||||
console.log('⚙️ Engine: sweetistics (fallback)');
|
||||
console.log('🔑 Credentials: Sweetistics API key');
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.error(`❌ Failed to determine current user: ${result.error ?? 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user