diff --git a/scripts/last30days.py b/scripts/last30days.py index 68448ac..8b6d08f 100644 --- a/scripts/last30days.py +++ b/scripts/last30days.py @@ -33,6 +33,11 @@ def ensure_supported_python(version_info: tuple[int, int, int] | object | None = ensure_supported_python() +if os.name == "nt": + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") + SCRIPT_DIR = Path(__file__).parent.resolve() sys.path.insert(0, str(SCRIPT_DIR)) diff --git a/scripts/lib/bird_x.py b/scripts/lib/bird_x.py index efd0a0b..d2b723f 100644 --- a/scripts/lib/bird_x.py +++ b/scripts/lib/bird_x.py @@ -177,6 +177,8 @@ def _run_bird_search(query: str, count: int, timeout: int) -> Dict[str, Any]: stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + encoding="utf-8", + errors="replace", preexec_fn=preexec, env=_subprocess_env(), ) @@ -336,6 +338,8 @@ def search_handles( stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + encoding="utf-8", + errors="replace", preexec_fn=preexec, env=_subprocess_env(), ) diff --git a/scripts/lib/env.py b/scripts/lib/env.py index 755a002..58042e5 100644 --- a/scripts/lib/env.py +++ b/scripts/lib/env.py @@ -579,6 +579,8 @@ def get_x_source_status(config: dict[str, Any]) -> dict[str, Any]: """ from . import bird_x + if config.get('AUTH_TOKEN') and config.get('CT0'): + bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0')) bird_status = bird_x.get_bird_status() xai_available = bool(config.get('XAI_API_KEY')) diff --git a/scripts/lib/vendor/bird-search/bird-search.mjs b/scripts/lib/vendor/bird-search/bird-search.mjs index 2f96c52..03cdde7 100644 --- a/scripts/lib/vendor/bird-search/bird-search.mjs +++ b/scripts/lib/vendor/bird-search/bird-search.mjs @@ -18,117 +18,130 @@ const SearchClient = withSearch(TwitterClientBase); const args = process.argv.slice(2); -// --check: verify that credentials can be resolved -if (args.includes('--check')) { +function writeStdout(text) { + if (text) process.stdout.write(text); +} + +function writeStderr(text) { + if (text) process.stderr.write(text); +} + +async function main() { + // --check: verify that credentials can be resolved + if (args.includes('--check')) { + try { + const { cookies, warnings } = await resolveCredentials({}); + if (cookies.authToken && cookies.ct0) { + writeStdout(JSON.stringify({ authenticated: true, source: cookies.source })); + return 0; + } + writeStdout(JSON.stringify({ authenticated: false, warnings })); + return 1; + } catch (err) { + writeStdout(JSON.stringify({ authenticated: false, error: err.message })); + return 1; + } + } + + // --whoami: check auth and output source + if (args.includes('--whoami')) { + try { + const { cookies } = await resolveCredentials({}); + if (cookies.authToken && cookies.ct0) { + writeStdout(cookies.source || 'authenticated'); + return 0; + } + writeStderr('Not authenticated\n'); + return 1; + } catch (err) { + writeStderr(`Auth check failed: ${err.message}\n`); + return 1; + } + } + + // Parse search args + let query = null; + let count = 20; + let jsonOutput = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--count' && args[i + 1]) { + count = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === '-n' && args[i + 1]) { + count = parseInt(args[i + 1], 10); + i++; + } else if (args[i] === '--json') { + jsonOutput = true; + } else if (!args[i].startsWith('-')) { + query = args[i]; + } + } + + if (!query) { + writeStderr('Usage: node bird-search.mjs [--count N] [--json]\n'); + return 1; + } + try { + // Resolve credentials (env vars, then browser cookies) const { cookies, warnings } = await resolveCredentials({}); - if (cookies.authToken && cookies.ct0) { - process.stdout.write(JSON.stringify({ authenticated: true, source: cookies.source })); - process.exit(0); - } else { - process.stdout.write(JSON.stringify({ authenticated: false, warnings })); - process.exit(1); + + if (!cookies.authToken || !cookies.ct0) { + const msg = warnings.length > 0 ? warnings.join('; ') : 'No Twitter credentials found'; + if (jsonOutput) { + writeStdout(JSON.stringify({ error: msg, items: [] })); + } else { + writeStderr(`Error: ${msg}\n`); + } + return 1; } - } catch (err) { - process.stdout.write(JSON.stringify({ authenticated: false, error: err.message })); - process.exit(1); - } -} -// --whoami: check auth and output source -if (args.includes('--whoami')) { - try { - const { cookies } = await resolveCredentials({}); - if (cookies.authToken && cookies.ct0) { - process.stdout.write(cookies.source || 'authenticated'); - process.exit(0); - } else { - process.stderr.write('Not authenticated\n'); - process.exit(1); + const client = new SearchClient({ + cookies: { + authToken: cookies.authToken, + ct0: cookies.ct0, + cookieHeader: cookies.cookieHeader, + }, + timeoutMs: 30000, + }); + + const result = await client.search(query, count); + + if (!result.success) { + if (jsonOutput) { + writeStdout(JSON.stringify({ error: result.error, items: [] })); + } else { + writeStderr(`Search failed: ${result.error}\n`); + } + return 1; } + + const tweets = result.tweets || []; + if (jsonOutput) { + writeStdout(JSON.stringify(tweets)); + } else { + for (const tweet of tweets) { + const author = tweet.author?.username || 'unknown'; + writeStdout(`@${author}: ${tweet.text?.slice(0, 200)}\n\n`); + } + } + + return 0; } catch (err) { - process.stderr.write(`Auth check failed: ${err.message}\n`); - process.exit(1); + if (jsonOutput) { + writeStdout(JSON.stringify({ error: err.message, items: [] })); + } else { + writeStderr(`Error: ${err.message}\n`); + } + return 1; } } -// Parse search args -let query = null; -let count = 20; -let jsonOutput = false; - -for (let i = 0; i < args.length; i++) { - if (args[i] === '--count' && args[i + 1]) { - count = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === '-n' && args[i + 1]) { - count = parseInt(args[i + 1], 10); - i++; - } else if (args[i] === '--json') { - jsonOutput = true; - } else if (!args[i].startsWith('-')) { - query = args[i]; - } -} - -if (!query) { - process.stderr.write('Usage: node bird-search.mjs [--count N] [--json]\n'); - process.exit(1); -} - try { - // Resolve credentials (env vars, then browser cookies) - const { cookies, warnings } = await resolveCredentials({}); - - if (!cookies.authToken || !cookies.ct0) { - const msg = warnings.length > 0 ? warnings.join('; ') : 'No Twitter credentials found'; - if (jsonOutput) { - process.stdout.write(JSON.stringify({ error: msg, items: [] })); - } else { - process.stderr.write(`Error: ${msg}\n`); - } - process.exit(1); - } - - // Create search client - const client = new SearchClient({ - cookies: { - authToken: cookies.authToken, - ct0: cookies.ct0, - cookieHeader: cookies.cookieHeader, - }, - timeoutMs: 30000, - }); - - // Run search - const result = await client.search(query, count); - - if (!result.success) { - if (jsonOutput) { - process.stdout.write(JSON.stringify({ error: result.error, items: [] })); - } else { - process.stderr.write(`Search failed: ${result.error}\n`); - } - process.exit(1); - } - - // Output results - const tweets = result.tweets || []; - if (jsonOutput) { - process.stdout.write(JSON.stringify(tweets)); - } else { - for (const tweet of tweets) { - const author = tweet.author?.username || 'unknown'; - process.stdout.write(`@${author}: ${tweet.text?.slice(0, 200)}\n\n`); - } - } - - process.exit(0); + const code = await main(); + process.exitCode = Number.isInteger(code) ? code : 1; } catch (err) { - if (jsonOutput) { - process.stdout.write(JSON.stringify({ error: err.message, items: [] })); - } else { - process.stderr.write(`Error: ${err.message}\n`); - } - process.exit(1); + writeStderr(`Fatal error: ${err?.message || err}\n`); + process.exitCode = 1; }