diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be58e4..4426589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - `mentions`: no hardcoded user; defaults to authenticated user or accepts `--user @handle`. - GraphQL query ID updater: correctly pairs `operationName` ↔ `queryId` (CreateTweet/CreateRetweet/etc). - `build:dist`: copies `src/lib/query-ids.json` into `dist/lib/query-ids.json` (keeps `dist/` in sync). +- `--engine graphql`: strict GraphQL-only (disables Sweetistics fallback). ## 0.1.1 — 2025-12-26 diff --git a/README.md b/README.md index 04dcdf8..82f06fb 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ bird query-ids --fresh - `--engine graphql` — use Twitter/X GraphQL with cookies (Chrome/Firefox/env/flags). - `--engine sweetistics` — use Sweetistics API key (no browser cookies needed). +Note: fallback to Sweetistics only happens in `--engine auto`. `--engine graphql` is strict (no fallback). + Global options: - `--timeout `: abort requests after the given timeout (milliseconds). - `--plain`: stable output (no emoji, no color). diff --git a/src/index.ts b/src/index.ts index 2d71043..c0ba0fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,12 @@ import JSON5 from 'json5'; import kleur from 'kleur'; import { resolveCliInvocation } from './lib/cli-args.js'; import { resolveCredentials } from './lib/cookies.js'; -import { type EngineMode, resolveEngineMode, shouldUseSweetistics } from './lib/engine.js'; +import { + type EngineMode, + resolveEngineMode, + shouldAllowSweetisticsFallback, + shouldUseSweetistics, +} from './lib/engine.js'; import { extractTweetId } from './lib/extract-tweet-id.js'; import { mentionsQueryFromUserOption, normalizeHandle } from './lib/normalize-handle.js'; import { @@ -429,7 +434,7 @@ program if (result.success) { console.log(`${p('ok')}Tweet posted successfully!`); console.log(`${l('url')}https://x.com/i/status/${result.tweetId}`); - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL tweet failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -558,7 +563,7 @@ program if (result.success) { console.log(`${p('ok')}Reply posted successfully!`); console.log(`${l('url')}https://x.com/i/status/${result.tweetId}`); - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL reply failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -657,7 +662,7 @@ program } console.log(formatStatsLine(result.tweet, output)); } - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL read failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -741,7 +746,7 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' }); - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL replies failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -816,7 +821,7 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' }); - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL thread failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -893,7 +898,7 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' }); - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL search failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -988,7 +993,7 @@ program const who = await client.getCurrentUser(); const handle = normalizeHandle(who.user?.username); if (!handle) { - if (sweetistics.apiKey) { + if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error( `${p('warn')}Could not determine current user (${who.error ?? 'Unknown error'}); trying Sweetistics fallback...`, ); @@ -1018,7 +1023,7 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); - } else if (sweetistics.apiKey) { + } else if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { console.error(`${p('warn')}GraphQL mentions failed (${result.error}); trying Sweetistics fallback...`); const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, @@ -1115,7 +1120,7 @@ program console.log(`${l('credentials')}${credentialSource}`); } else { // Fallback: try Sweetistics if available - if (sweetistics.apiKey) { + if (shouldAllowSweetisticsFallback(engine, Boolean(sweetistics.apiKey))) { const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey, diff --git a/src/lib/engine.ts b/src/lib/engine.ts index 54cf2fb..c303625 100644 --- a/src/lib/engine.ts +++ b/src/lib/engine.ts @@ -13,3 +13,7 @@ export function shouldUseSweetistics(engine: EngineMode, _hasApiKey: boolean): b if (engine === 'graphql') return false; return false; // auto: GraphQL primary, Sweetistics only as fallback } + +export function shouldAllowSweetisticsFallback(engine: EngineMode, hasApiKey: boolean): boolean { + return engine === 'auto' && hasApiKey; +} diff --git a/tests/engine.test.ts b/tests/engine.test.ts index cf5f614..5c745f2 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { resolveEngineMode, shouldUseSweetistics } from '../src/lib/engine.js'; +import { resolveEngineMode, shouldAllowSweetisticsFallback, shouldUseSweetistics } from '../src/lib/engine.js'; describe('engine', () => { it('normalizes engine values and defaults to auto', () => { @@ -19,4 +19,11 @@ describe('engine', () => { expect(shouldUseSweetistics('sweetistics', false)).toBe(true); expect(shouldUseSweetistics('graphql', true)).toBe(false); }); + + it('only allows Sweetistics fallback in auto mode', () => { + expect(shouldAllowSweetisticsFallback('auto', true)).toBe(true); + expect(shouldAllowSweetisticsFallback('auto', false)).toBe(false); + expect(shouldAllowSweetisticsFallback('graphql', true)).toBe(false); + expect(shouldAllowSweetisticsFallback('sweetistics', true)).toBe(false); + }); });