feat: default to graphql+firefox, fallback to sweetistics

This commit is contained in:
Peter Steinberger
2025-12-03 16:29:16 +00:00
parent 5797c00882
commit e0ca146567
2 changed files with 144 additions and 25 deletions
+12 -15
View File
@@ -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
View File
@@ -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);
}