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:
|
Transport (engine) selection:
|
||||||
- `--engine graphql|sweetistics|auto` (default `auto`).
|
- `--engine graphql|sweetistics|auto` (default `graphql`).
|
||||||
- `sweetistics`: use Sweetistics API key, no browser cookies needed.
|
- `sweetistics`: use Sweetistics API key, no browser cookies needed.
|
||||||
- `graphql`: use Twitter/X GraphQL with cookies (Chrome/Firefox/env/flags).
|
- `graphql`: use Twitter/X GraphQL with cookies (Chrome/Firefox/env/flags).
|
||||||
- `auto`: Sweetistics if an API key is available, otherwise GraphQL.
|
- `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`
|
- Global: `~/.config/bird/config.json5`
|
||||||
- Project: `./.birdrc.json5` (overrides global)
|
- Project: `./.birdrc.json5` (overrides global)
|
||||||
|
|
||||||
Example `~/.config/bird/config.json5`:
|
Example `~/.config/bird/config.json5` (Firefox + GraphQL defaults):
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
// Default to Sweetistics unless overridden by --engine or BIRD_ENGINE
|
engine: "graphql",
|
||||||
engine: "sweetistics",
|
|
||||||
// Prefer Firefox cookies by default
|
// Prefer Firefox cookies by default
|
||||||
firefoxProfile: "default-release",
|
firefoxProfile: "default-release",
|
||||||
// Optional: Sweetistics defaults
|
// Optional: Sweetistics defaults if you want fallback/overrides
|
||||||
sweetisticsApiKey: "sweet-...",
|
sweetisticsApiKey: "sweet-..."
|
||||||
sweetisticsBaseUrl: "https://sweetistics.com"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Precedence: CLI flags > environment variables > project config > global config.
|
Precedence: CLI flags > environment variables > project config > global config.
|
||||||
|
|
||||||
To default to Firefox and Sweetistics, create `~/.config/bird/config.json5`:
|
### Credential sources (macOS)
|
||||||
|
|
||||||
```json5
|
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).
|
||||||
engine: "sweetistics",
|
- **Chrome**: `~/Library/Application Support/Google/Chrome/<Profile>/Cookies` (WAL/SHM copied too). `--chrome-profile <name>`.
|
||||||
firefoxProfile: "default-release",
|
- **Env/flags** always override browser cookies.
|
||||||
sweetisticsApiKey: "sweet-..."
|
|
||||||
}
|
Transport is chosen first; then, if transport is GraphQL, the cookie source is resolved with the same precedence.
|
||||||
```
|
|
||||||
|
|
||||||
### Post a tweet
|
### Post a tweet
|
||||||
|
|
||||||
|
|||||||
+132
-10
@@ -98,7 +98,11 @@ program
|
|||||||
.option('--auth-token <token>', 'Twitter auth_token cookie')
|
.option('--auth-token <token>', 'Twitter auth_token cookie')
|
||||||
.option('--ct0 <token>', 'Twitter ct0 cookie')
|
.option('--ct0 <token>', 'Twitter ct0 cookie')
|
||||||
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction', config.chromeProfile)
|
.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-api-key <key>', 'Sweetistics API key (or set SWEETISTICS_API_KEY)')
|
||||||
.option(
|
.option(
|
||||||
'--sweetistics-base-url <url>',
|
'--sweetistics-base-url <url>',
|
||||||
@@ -108,7 +112,7 @@ program
|
|||||||
.option(
|
.option(
|
||||||
'--engine <engine>',
|
'--engine <engine>',
|
||||||
'Engine: graphql | sweetistics | auto',
|
'Engine: graphql | sweetistics | auto',
|
||||||
process.env.BIRD_ENGINE || config.engine || 'auto',
|
process.env.BIRD_ENGINE || config.engine || 'graphql',
|
||||||
);
|
);
|
||||||
|
|
||||||
type EngineMode = 'graphql' | 'sweetistics' | 'auto';
|
type EngineMode = 'graphql' | 'sweetistics' | 'auto';
|
||||||
@@ -227,6 +231,21 @@ program
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('✅ Tweet posted successfully!');
|
console.log('✅ Tweet posted successfully!');
|
||||||
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
|
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 {
|
} else {
|
||||||
console.error(`❌ Failed to post tweet: ${result.error}`);
|
console.error(`❌ Failed to post tweet: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -303,6 +322,21 @@ program
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('✅ Reply posted successfully!');
|
console.log('✅ Reply posted successfully!');
|
||||||
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
|
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 {
|
} else {
|
||||||
console.error(`❌ Failed to post reply: ${result.error}`);
|
console.error(`❌ Failed to post reply: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -383,6 +417,28 @@ program
|
|||||||
`❤️ ${result.tweet.likeCount ?? 0} 🔁 ${result.tweet.retweetCount ?? 0} 💬 ${result.tweet.replyCount ?? 0}`,
|
`❤️ ${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 {
|
} else {
|
||||||
console.error(`❌ Failed to read tweet: ${result.error}`);
|
console.error(`❌ Failed to read tweet: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -440,6 +496,18 @@ program
|
|||||||
|
|
||||||
if (result.success && result.tweets) {
|
if (result.success && result.tweets) {
|
||||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
|
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 {
|
} else {
|
||||||
console.error(`❌ Failed to fetch replies: ${result.error}`);
|
console.error(`❌ Failed to fetch replies: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -497,6 +565,17 @@ program
|
|||||||
|
|
||||||
if (result.success && result.tweets) {
|
if (result.success && result.tweets) {
|
||||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
|
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 {
|
} else {
|
||||||
console.error(`❌ Failed to fetch thread: ${result.error}`);
|
console.error(`❌ Failed to fetch thread: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -556,6 +635,18 @@ program
|
|||||||
|
|
||||||
if (result.success && result.tweets) {
|
if (result.success && result.tweets) {
|
||||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
|
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 {
|
} else {
|
||||||
console.error(`❌ Search failed: ${result.error}`);
|
console.error(`❌ Search failed: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -610,6 +701,18 @@ program
|
|||||||
|
|
||||||
if (result.success && result.tweets) {
|
if (result.success && result.tweets) {
|
||||||
printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
|
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 {
|
} else {
|
||||||
console.error(`❌ Failed to fetch mentions: ${result.error}`);
|
console.error(`❌ Failed to fetch mentions: ${result.error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -635,14 +738,14 @@ program
|
|||||||
? 'Sweetistics API key'
|
? 'Sweetistics API key'
|
||||||
: 'none'
|
: 'none'
|
||||||
: opts.chromeProfile
|
: opts.chromeProfile
|
||||||
? `Chrome profile "${opts.chromeProfile}"`
|
? `Chrome profile "${opts.chromeProfile}"`
|
||||||
: opts.firefoxProfile
|
: opts.firefoxProfile
|
||||||
? `Firefox profile "${opts.firefoxProfile}"`
|
? `Firefox profile "${opts.firefoxProfile}"`
|
||||||
: config.chromeProfile
|
: config.chromeProfile
|
||||||
? `Chrome profile "${config.chromeProfile}"`
|
? `Chrome profile "${config.chromeProfile}"`
|
||||||
: config.firefoxProfile
|
: config.firefoxProfile
|
||||||
? `Firefox profile "${config.firefoxProfile}"`
|
? `Firefox profile "${config.firefoxProfile}"`
|
||||||
: 'env/auto-detected cookies';
|
: 'env/auto-detected cookies';
|
||||||
|
|
||||||
if (useSweetistics) {
|
if (useSweetistics) {
|
||||||
if (!sweetistics.apiKey) {
|
if (!sweetistics.apiKey) {
|
||||||
@@ -699,6 +802,25 @@ program
|
|||||||
console.log(`⚙️ Engine: ${resolvedEngine}`);
|
console.log(`⚙️ Engine: ${resolvedEngine}`);
|
||||||
console.log(`🔑 Credentials: ${credentialSource}`);
|
console.log(`🔑 Credentials: ${credentialSource}`);
|
||||||
} else {
|
} 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'}`);
|
console.error(`❌ Failed to determine current user: ${result.error ?? 'Unknown error'}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user