This commit is contained in:
@@ -134,7 +134,8 @@ By default, the command fetches from For You, News, Sports, and Entertainment ta
|
|||||||
import { TwitterClient } from '@yuta/bird';
|
import { TwitterClient } from '@yuta/bird';
|
||||||
|
|
||||||
const client = new TwitterClient({
|
const client = new TwitterClient({
|
||||||
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL
|
relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL,
|
||||||
|
profileName: process.env.BIRD_PROFILE_NAME
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search for tweets
|
// Search for tweets
|
||||||
@@ -185,6 +186,7 @@ const sportsNews = await client.getNews(10, {
|
|||||||
|
|
||||||
Global options:
|
Global options:
|
||||||
- `--relay-base-url <url>`: twitter safe relay base URL (defaults to `TWITTER_RELAY_BASE_URL`).
|
- `--relay-base-url <url>`: twitter safe relay base URL (defaults to `TWITTER_RELAY_BASE_URL`).
|
||||||
|
- `--profile-name <name>`: twitter safe relay profile name (sent as `x-profile-name`).
|
||||||
- `--timeout <ms>`: abort requests after the given timeout (milliseconds).
|
- `--timeout <ms>`: abort requests after the given timeout (milliseconds).
|
||||||
- `--quote-depth <n>`: max quoted tweet depth in JSON output (default: 1; 0 disables).
|
- `--quote-depth <n>`: max quoted tweet depth in JSON output (default: 1; 0 disables).
|
||||||
- `--plain`: stable output (no emoji, no color).
|
- `--plain`: stable output (no emoji, no color).
|
||||||
@@ -203,6 +205,8 @@ Write operations:
|
|||||||
- If GraphQL returns error `226` (“automated request”), `bird` falls back to the legacy `statuses/update.json` endpoint.
|
- If GraphQL returns error `226` (“automated request”), `bird` falls back to the legacy `statuses/update.json` endpoint.
|
||||||
|
|
||||||
Configure the relay URL with `TWITTER_RELAY_BASE_URL` or `--relay-base-url`.
|
Configure the relay URL with `TWITTER_RELAY_BASE_URL` or `--relay-base-url`.
|
||||||
|
When your relay has multiple profiles, select one with `--profile-name`, `BIRD_PROFILE_NAME`, or `profileName`
|
||||||
|
in config; bird forwards it as the `x-profile-name` header.
|
||||||
|
|
||||||
## Config (JSON5)
|
## Config (JSON5)
|
||||||
|
|
||||||
@@ -216,6 +220,7 @@ Example `~/.config/bird/config.json5`:
|
|||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
relayBaseUrl: "http://localhost:6900",
|
relayBaseUrl: "http://localhost:6900",
|
||||||
|
profileName: "main",
|
||||||
timeoutMs: 20000,
|
timeoutMs: 20000,
|
||||||
quoteDepth: 1
|
quoteDepth: 1
|
||||||
}
|
}
|
||||||
@@ -223,6 +228,7 @@ Example `~/.config/bird/config.json5`:
|
|||||||
|
|
||||||
Environment shortcuts:
|
Environment shortcuts:
|
||||||
- `TWITTER_RELAY_BASE_URL`
|
- `TWITTER_RELAY_BASE_URL`
|
||||||
|
- `BIRD_PROFILE_NAME`
|
||||||
- `BIRD_TIMEOUT_MS`
|
- `BIRD_TIMEOUT_MS`
|
||||||
- `BIRD_QUOTE_DEPTH`
|
- `BIRD_QUOTE_DEPTH`
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -105,14 +105,15 @@ export function createProgram(ctx: CliContext): Command {
|
|||||||
`\n\n${ctx.colors.section('Config')}\n${ctx.colors.muted(
|
`\n\n${ctx.colors.section('Config')}\n${ctx.colors.muted(
|
||||||
` Reads ${ctx.colors.argument('~/.config/bird/config.json5')} and ${ctx.colors.argument('./.birdrc.json5')} (JSON5)`,
|
` Reads ${ctx.colors.argument('~/.config/bird/config.json5')} and ${ctx.colors.argument('./.birdrc.json5')} (JSON5)`,
|
||||||
)}\n${ctx.colors.muted(
|
)}\n${ctx.colors.muted(
|
||||||
` Supports: relayBaseUrl, timeoutMs, quoteDepth`,
|
` Supports: relayBaseUrl, profileName, timeoutMs, quoteDepth`,
|
||||||
)}\n\n${ctx.colors.section('Env')}\n${ctx.colors.muted(
|
)}\n\n${ctx.colors.section('Env')}\n${ctx.colors.muted(
|
||||||
` ${ctx.colors.option('TWITTER_RELAY_BASE_URL')}, ${ctx.colors.option('NO_COLOR')}, ${ctx.colors.option('BIRD_TIMEOUT_MS')}, ${ctx.colors.option('BIRD_QUOTE_DEPTH')}`,
|
` ${ctx.colors.option('TWITTER_RELAY_BASE_URL')}, ${ctx.colors.option('BIRD_PROFILE_NAME')}, ${ctx.colors.option('NO_COLOR')}, ${ctx.colors.option('BIRD_TIMEOUT_MS')}, ${ctx.colors.option('BIRD_QUOTE_DEPTH')}`,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
program
|
program
|
||||||
.option('--relay-base-url <url>', 'twitter safe relay base URL', ctx.config.relayBaseUrl)
|
.option('--relay-base-url <url>', 'twitter safe relay base URL', ctx.config.relayBaseUrl)
|
||||||
|
.option('--profile-name <name>', 'twitter safe relay profile name')
|
||||||
.option('--media <path>', 'Attach media file (repeatable, up to 4 images or 1 video)', collect)
|
.option('--media <path>', 'Attach media file (repeatable, up to 4 images or 1 video)', collect)
|
||||||
.option('--alt <text>', 'Alt text for the corresponding --media (repeatable)', collect)
|
.option('--alt <text>', 'Alt text for the corresponding --media (repeatable)', collect)
|
||||||
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
.option('--timeout <ms>', 'Request timeout in milliseconds')
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { TweetData } from '../lib/twitter-client.js';
|
|||||||
|
|
||||||
export type BirdConfig = {
|
export type BirdConfig = {
|
||||||
relayBaseUrl?: string;
|
relayBaseUrl?: string;
|
||||||
|
profileName?: string;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
quoteDepth?: number;
|
quoteDepth?: number;
|
||||||
};
|
};
|
||||||
@@ -44,6 +45,7 @@ export type CliContext = {
|
|||||||
applyOutputFromCommand: (command: Command) => void;
|
applyOutputFromCommand: (command: Command) => void;
|
||||||
resolveTimeoutFromOptions: (options: { timeout?: string | number }) => number | undefined;
|
resolveTimeoutFromOptions: (options: { timeout?: string | number }) => number | undefined;
|
||||||
resolveQuoteDepthFromOptions: (options: { quoteDepth?: string | number }) => number | undefined;
|
resolveQuoteDepthFromOptions: (options: { quoteDepth?: string | number }) => number | undefined;
|
||||||
|
resolveProfileNameFromOptions: (opts: ProfileOptions) => string | undefined;
|
||||||
resolveRelayBaseUrlFromOptions: (opts: RelayOptions) => string | undefined;
|
resolveRelayBaseUrlFromOptions: (opts: RelayOptions) => string | undefined;
|
||||||
loadMedia: (opts: { media: string[]; alts: string[] }) => MediaSpec[];
|
loadMedia: (opts: { media: string[]; alts: string[] }) => MediaSpec[];
|
||||||
printTweets: (tweets: TweetData[], opts?: { json?: boolean; emptyMessage?: string; showSeparator?: boolean }) => void;
|
printTweets: (tweets: TweetData[], opts?: { json?: boolean; emptyMessage?: string; showSeparator?: boolean }) => void;
|
||||||
@@ -138,6 +140,10 @@ type RelayOptions = {
|
|||||||
relayBaseUrl?: string;
|
relayBaseUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ProfileOptions = {
|
||||||
|
profileName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function createCliContext(normalizedArgs: string[], env: NodeJS.ProcessEnv = process.env): CliContext {
|
export function createCliContext(normalizedArgs: string[], env: NodeJS.ProcessEnv = process.env): CliContext {
|
||||||
const isTty = process.stdout.isTTY;
|
const isTty = process.stdout.isTTY;
|
||||||
let output: OutputConfig = resolveOutputConfigFromArgv(normalizedArgs, env, isTty);
|
let output: OutputConfig = resolveOutputConfigFromArgv(normalizedArgs, env, isTty);
|
||||||
@@ -235,6 +241,10 @@ export function createCliContext(normalizedArgs: string[], env: NodeJS.ProcessEn
|
|||||||
return opts.relayBaseUrl || config.relayBaseUrl || env.TWITTER_RELAY_BASE_URL;
|
return opts.relayBaseUrl || config.relayBaseUrl || env.TWITTER_RELAY_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveProfileNameFromOptions(opts: ProfileOptions): string | undefined {
|
||||||
|
return opts.profileName || env.BIRD_PROFILE_NAME || config.profileName;
|
||||||
|
}
|
||||||
|
|
||||||
function loadMedia(opts: { media: string[]; alts: string[] }): MediaSpec[] {
|
function loadMedia(opts: { media: string[]; alts: string[] }): MediaSpec[] {
|
||||||
if (opts.media.length === 0) {
|
if (opts.media.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -367,6 +377,7 @@ export function createCliContext(normalizedArgs: string[], env: NodeJS.ProcessEn
|
|||||||
applyOutputFromCommand,
|
applyOutputFromCommand,
|
||||||
resolveTimeoutFromOptions,
|
resolveTimeoutFromOptions,
|
||||||
resolveQuoteDepthFromOptions,
|
resolveQuoteDepthFromOptions,
|
||||||
|
resolveProfileNameFromOptions,
|
||||||
resolveRelayBaseUrlFromOptions,
|
resolveRelayBaseUrlFromOptions,
|
||||||
loadMedia,
|
loadMedia,
|
||||||
printTweets,
|
printTweets,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
|
|||||||
}
|
}
|
||||||
const maxPages = maxPagesParsed.value;
|
const maxPages = maxPagesParsed.value;
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const usePagination = Boolean(cmdOpts.all || cmdOpts.cursor);
|
const usePagination = Boolean(cmdOpts.all || cmdOpts.cursor);
|
||||||
if (maxPages !== undefined && !usePagination) {
|
if (maxPages !== undefined && !usePagination) {
|
||||||
@@ -46,7 +47,7 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
const folderId = cmdOpts.folderId ? extractBookmarkFolderId(cmdOpts.folderId) : null;
|
const folderId = cmdOpts.folderId ? extractBookmarkFolderId(cmdOpts.folderId) : null;
|
||||||
if (cmdOpts.folderId && !folderId) {
|
if (cmdOpts.folderId && !folderId) {
|
||||||
console.error(`${ctx.p('err')}Invalid --folder-id. Expected numeric ID or https://x.com/i/bookmarks/<id>.`);
|
console.error(`${ctx.p('err')}Invalid --folder-id. Expected numeric ID or https://x.com/i/bookmarks/<id>.`);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export function registerCheckCommand(program: Command, ctx: CliContext): void {
|
|||||||
const opts = program.opts();
|
const opts = program.opts();
|
||||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
console.log(`${ctx.p('info')}Relay check`);
|
console.log(`${ctx.p('info')}Relay check`);
|
||||||
console.log('-'.repeat(40));
|
console.log('-'.repeat(40));
|
||||||
@@ -21,7 +22,7 @@ export function registerCheckCommand(program: Command, ctx: CliContext): void {
|
|||||||
|
|
||||||
console.log(`${ctx.p('ok')}relay: ${relayBaseUrl}`);
|
console.log(`${ctx.p('ok')}relay: ${relayBaseUrl}`);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
const result = await client.getCurrentUser();
|
const result = await client.getCurrentUser();
|
||||||
|
|
||||||
if (result.success && result.user) {
|
if (result.success && result.user) {
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ export function registerHomeCommand(program: Command, ctx: CliContext): void {
|
|||||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||||
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
if (!Number.isFinite(count) || count <= 0) {
|
if (!Number.isFinite(count) || count <= 0) {
|
||||||
console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`);
|
console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
|
|
||||||
const result = cmdOpts.following
|
const result = cmdOpts.following
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ export function registerListsCommand(program: Command, ctx: CliContext): void {
|
|||||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||||
const count = Number.parseInt(cmdOpts.count || '100', 10);
|
const count = Number.parseInt(cmdOpts.count || '100', 10);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
|
|
||||||
const result = cmdOpts.memberOf ? await client.getListMemberships(count) : await client.getOwnedLists(count);
|
const result = cmdOpts.memberOf ? await client.getListMemberships(count) : await client.getOwnedLists(count);
|
||||||
|
|
||||||
@@ -109,8 +110,9 @@ export function registerListsCommand(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
const timelineOptions = { includeRaw };
|
const timelineOptions = { includeRaw };
|
||||||
const paginationOptions = { includeRaw, maxPages: pagination.maxPages, cursor: pagination.cursor };
|
const paginationOptions = { includeRaw, maxPages: pagination.maxPages, cursor: pagination.cursor };
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export function registerNewsCommand(program: Command, ctx: CliContext): void {
|
|||||||
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
||||||
const tweetsPerItem = Number.parseInt(cmdOpts.tweetsPerItem || '5', 10);
|
const tweetsPerItem = Number.parseInt(cmdOpts.tweetsPerItem || '5', 10);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
if (Number.isNaN(count) || count < 1) {
|
if (Number.isNaN(count) || count < 1) {
|
||||||
console.error(`${ctx.p('err')}--count must be a positive number`);
|
console.error(`${ctx.p('err')}--count must be a positive number`);
|
||||||
@@ -134,7 +135,7 @@ export function registerNewsCommand(program: Command, ctx: CliContext): void {
|
|||||||
// If no specific tabs selected, use defaults (all tabs except trending)
|
// If no specific tabs selected, use defaults (all tabs except trending)
|
||||||
const tabsToFetch = tabs.length > 0 ? tabs : undefined;
|
const tabsToFetch = tabs.length > 0 ? tabs : undefined;
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
const withTweets = cmdOpts.withTweets ?? false;
|
const withTweets = cmdOpts.withTweets ?? false;
|
||||||
const aiOnly = cmdOpts.aiOnly ?? false;
|
const aiOnly = cmdOpts.aiOnly ?? false;
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export function registerPostCommands(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const mediaIds = await uploadMediaOrExit(client, media, ctx);
|
const mediaIds = await uploadMediaOrExit(client, media, ctx);
|
||||||
const result = await client.tweet(text, mediaIds);
|
const result = await client.tweet(text, mediaIds);
|
||||||
|
|
||||||
@@ -72,9 +73,10 @@ export function registerPostCommands(program: Command, ctx: CliContext): void {
|
|||||||
}
|
}
|
||||||
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
console.error(`${ctx.p('info')}Replying to tweet: ${tweetId}`);
|
console.error(`${ctx.p('info')}Replying to tweet: ${tweetId}`);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const mediaIds = await uploadMediaOrExit(client, media, ctx);
|
const mediaIds = await uploadMediaOrExit(client, media, ctx);
|
||||||
const result = await client.reply(text, tweetId, mediaIds);
|
const result = await client.reply(text, tweetId, mediaIds);
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
|
|||||||
|
|
||||||
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
const tweetId = ctx.extractTweetId(tweetIdOrUrl);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
const result = await client.getTweet(tweetId, { includeRaw });
|
const result = await client.getTweet(tweetId, { includeRaw });
|
||||||
|
|
||||||
@@ -69,8 +70,9 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
|
|
||||||
const result = pagination.usePagination
|
const result = pagination.usePagination
|
||||||
@@ -136,8 +138,9 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
|
|
||||||
const result = pagination.usePagination
|
const result = pagination.usePagination
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
|
|||||||
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
const count = Number.parseInt(cmdOpts.count || '10', 10);
|
||||||
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
|
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const usePagination = cmdOpts.all || cmdOpts.cursor;
|
const usePagination = cmdOpts.all || cmdOpts.cursor;
|
||||||
if (maxPages !== undefined && !usePagination) {
|
if (maxPages !== undefined && !usePagination) {
|
||||||
@@ -47,7 +48,7 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
const searchOptions = { includeRaw };
|
const searchOptions = { includeRaw };
|
||||||
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
|
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
|
||||||
@@ -90,8 +91,9 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
|
|||||||
|
|
||||||
let query: string | null = fromUserOpt.query;
|
let query: string | null = fromUserOpt.query;
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
const who = await client.getCurrentUser();
|
const who = await client.getCurrentUser();
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export function registerUnbookmarkCommand(program: Command, ctx: CliContext): vo
|
|||||||
const opts = program.opts();
|
const opts = program.opts();
|
||||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
let failures = 0;
|
let failures = 0;
|
||||||
|
|
||||||
for (const input of tweetIdOrUrls) {
|
for (const input of tweetIdOrUrls) {
|
||||||
|
|||||||
@@ -87,8 +87,9 @@ export function registerUserTweetsCommand(program: Command, ctx: CliContext): vo
|
|||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
|
|
||||||
// Look up user ID from username
|
// Look up user ID from username
|
||||||
console.error(`${ctx.p('info')}Looking up @${username}...`);
|
console.error(`${ctx.p('info')}Looking up @${username}...`);
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
|
|
||||||
let userId = cmdOpts.user;
|
let userId = cmdOpts.user;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -187,8 +188,9 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs });
|
||||||
|
|
||||||
let userId = cmdOpts.user;
|
let userId = cmdOpts.user;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -328,6 +330,7 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
const count = Number.parseInt(cmdOpts.count || '20', 10);
|
||||||
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
|
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const usePagination = cmdOpts.all || cmdOpts.cursor;
|
const usePagination = cmdOpts.all || cmdOpts.cursor;
|
||||||
if (maxPages !== undefined && !usePagination) {
|
if (maxPages !== undefined && !usePagination) {
|
||||||
@@ -343,7 +346,7 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const includeRaw = cmdOpts.jsonFull ?? false;
|
const includeRaw = cmdOpts.jsonFull ?? false;
|
||||||
const timelineOptions = { includeRaw };
|
const timelineOptions = { includeRaw };
|
||||||
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
|
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
|
||||||
@@ -373,8 +376,9 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
|
|||||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||||
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
|
||||||
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
const relayBaseUrl = ctx.resolveRelayBaseUrlFromOptions(opts);
|
||||||
|
const profileName = ctx.resolveProfileNameFromOptions(opts);
|
||||||
|
|
||||||
const client = new TwitterClient({ relayBaseUrl, timeoutMs, quoteDepth });
|
const client = new TwitterClient({ relayBaseUrl, profileName, timeoutMs, quoteDepth });
|
||||||
const result = await client.getCurrentUser();
|
const result = await client.getCurrentUser();
|
||||||
|
|
||||||
if (result.success && result.user) {
|
if (result.success && result.user) {
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ export type Mixin<TBase extends AbstractConstructor<TwitterClientBase>, TAdded>
|
|||||||
|
|
||||||
export abstract class TwitterClientBase {
|
export abstract class TwitterClientBase {
|
||||||
protected relayBaseUrl: string;
|
protected relayBaseUrl: string;
|
||||||
|
protected profileName?: string;
|
||||||
protected timeoutMs?: number;
|
protected timeoutMs?: number;
|
||||||
protected quoteDepth: number;
|
protected quoteDepth: number;
|
||||||
protected clientUserId?: string;
|
protected clientUserId?: string;
|
||||||
|
|
||||||
constructor(options: TwitterClientOptions) {
|
constructor(options: TwitterClientOptions) {
|
||||||
this.relayBaseUrl = this.normalizeRelayBaseUrl(options.relayBaseUrl ?? process.env.TWITTER_RELAY_BASE_URL);
|
this.relayBaseUrl = this.normalizeRelayBaseUrl(options.relayBaseUrl ?? process.env.TWITTER_RELAY_BASE_URL);
|
||||||
|
this.profileName = this.normalizeProfileName(options.profileName);
|
||||||
this.timeoutMs = options.timeoutMs;
|
this.timeoutMs = options.timeoutMs;
|
||||||
this.quoteDepth = normalizeQuoteDepth(options.quoteDepth);
|
this.quoteDepth = normalizeQuoteDepth(options.quoteDepth);
|
||||||
}
|
}
|
||||||
@@ -73,7 +75,7 @@ export abstract class TwitterClientBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getHeaders(): Record<string, string> {
|
protected getHeaders(): Record<string, string> {
|
||||||
return {};
|
return this.profileName ? { 'x-profile-name': this.profileName } : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeRelayBaseUrl(value: string | undefined): string {
|
private normalizeRelayBaseUrl(value: string | undefined): string {
|
||||||
@@ -84,6 +86,11 @@ export abstract class TwitterClientBase {
|
|||||||
return trimmed.replace(TRAILING_SLASHES, '');
|
return trimmed.replace(TRAILING_SLASHES, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeProfileName(value: string | undefined): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
return trimmed || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private toRelayUrl(url: string): string {
|
private toRelayUrl(url: string): string {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
if (!['x.com', 'twitter.com', 'api.twitter.com', 'upload.twitter.com'].includes(parsed.hostname)) {
|
if (!['x.com', 'twitter.com', 'api.twitter.com', 'upload.twitter.com'].includes(parsed.hostname)) {
|
||||||
@@ -93,11 +100,11 @@ export abstract class TwitterClientBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getJsonHeaders(): Record<string, string> {
|
protected getJsonHeaders(): Record<string, string> {
|
||||||
return { 'content-type': 'application/json' };
|
return { 'content-type': 'application/json', ...this.getHeaders() };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getUploadHeaders(): Record<string, string> {
|
protected getUploadHeaders(): Record<string, string> {
|
||||||
return {};
|
return this.getHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async ensureClientUserId(): Promise<void> {
|
protected async ensureClientUserId(): Promise<void> {
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ export interface FollowingResult {
|
|||||||
|
|
||||||
export interface TwitterClientOptions {
|
export interface TwitterClientOptions {
|
||||||
relayBaseUrl?: string;
|
relayBaseUrl?: string;
|
||||||
|
profileName?: string;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
// Max depth for quoted tweets (0 disables). Defaults to 1.
|
// Max depth for quoted tweets (0 disables). Defaults to 1.
|
||||||
quoteDepth?: number;
|
quoteDepth?: number;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { createCliContext } from '../src/cli/shared.js';
|
||||||
|
|
||||||
|
describe('createCliContext', () => {
|
||||||
|
it('resolves profileName from CLI options before env', () => {
|
||||||
|
const ctx = createCliContext([], { BIRD_PROFILE_NAME: 'env-profile' } as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(ctx.resolveProfileNameFromOptions({ profileName: 'cli-profile' })).toBe('cli-profile');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves profileName from env when CLI option is absent', () => {
|
||||||
|
const ctx = createCliContext([], { BIRD_PROFILE_NAME: 'env-profile' } as NodeJS.ProcessEnv);
|
||||||
|
|
||||||
|
expect(ctx.resolveProfileNameFromOptions({})).toBe('env-profile');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -9,6 +9,7 @@ describe('bookmarks command', () => {
|
|||||||
const ctx = {
|
const ctx = {
|
||||||
resolveTimeoutFromOptions: () => undefined,
|
resolveTimeoutFromOptions: () => undefined,
|
||||||
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
||||||
|
resolveProfileNameFromOptions: () => undefined,
|
||||||
p: () => '',
|
p: () => '',
|
||||||
printTweets: () => undefined,
|
printTweets: () => undefined,
|
||||||
} as unknown as CliContext;
|
} as unknown as CliContext;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe('news command', () => {
|
|||||||
resolveTimeoutFromOptions: () => 30000,
|
resolveTimeoutFromOptions: () => 30000,
|
||||||
resolveQuoteDepthFromOptions: () => undefined,
|
resolveQuoteDepthFromOptions: () => undefined,
|
||||||
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
||||||
|
resolveProfileNameFromOptions: () => undefined,
|
||||||
p: (type: string) => `[${type}] `,
|
p: (type: string) => `[${type}] `,
|
||||||
colors: {
|
colors: {
|
||||||
accent: (text: string) => text,
|
accent: (text: string) => text,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe('replies command', () => {
|
|||||||
({
|
({
|
||||||
resolveTimeoutFromOptions: () => undefined,
|
resolveTimeoutFromOptions: () => undefined,
|
||||||
resolveQuoteDepthFromOptions: () => 1,
|
resolveQuoteDepthFromOptions: () => 1,
|
||||||
|
resolveProfileNameFromOptions: () => undefined,
|
||||||
extractTweetId: (input: string) => input,
|
extractTweetId: (input: string) => input,
|
||||||
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
||||||
p: () => '',
|
p: () => '',
|
||||||
@@ -74,6 +75,38 @@ describe('replies command', () => {
|
|||||||
errorSpy.mockRestore();
|
errorSpy.mockRestore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes profileName to TwitterClient', async () => {
|
||||||
|
const program = new Command();
|
||||||
|
registerReadCommands(program, {
|
||||||
|
...createMockContext(),
|
||||||
|
resolveProfileNameFromOptions: () => 'work',
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTweetSpy = vi.spyOn(TwitterClient.prototype, 'getTweet').mockImplementation(async function () {
|
||||||
|
const headers = (this as unknown as { getHeaders: () => Record<string, string> }).getHeaders();
|
||||||
|
expect(headers).toEqual({ 'x-profile-name': 'work' });
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
tweet: {
|
||||||
|
id: '123',
|
||||||
|
text: 'hello',
|
||||||
|
author: { username: 'tester', name: 'Tester' },
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
url: 'https://x.com/tester/status/123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await program.parseAsync(['node', 'bird', 'read', '123', '--json']);
|
||||||
|
expect(getTweetSpy).toHaveBeenCalledTimes(1);
|
||||||
|
} finally {
|
||||||
|
getTweetSpy.mockRestore();
|
||||||
|
logSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('thread command', () => {
|
describe('thread command', () => {
|
||||||
@@ -81,6 +114,7 @@ describe('thread command', () => {
|
|||||||
({
|
({
|
||||||
resolveTimeoutFromOptions: () => undefined,
|
resolveTimeoutFromOptions: () => undefined,
|
||||||
resolveQuoteDepthFromOptions: () => 1,
|
resolveQuoteDepthFromOptions: () => 1,
|
||||||
|
resolveProfileNameFromOptions: () => undefined,
|
||||||
extractTweetId: (input: string) => input,
|
extractTweetId: (input: string) => input,
|
||||||
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
||||||
p: () => '',
|
p: () => '',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe('search command', () => {
|
|||||||
resolveTimeoutFromOptions: () => undefined,
|
resolveTimeoutFromOptions: () => undefined,
|
||||||
resolveQuoteDepthFromOptions: () => undefined,
|
resolveQuoteDepthFromOptions: () => undefined,
|
||||||
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
||||||
|
resolveProfileNameFromOptions: () => undefined,
|
||||||
p: () => '',
|
p: () => '',
|
||||||
printTweetsResult: vi.fn(),
|
printTweetsResult: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { TwitterClient } from '../src/lib/twitter-client.js';
|
|||||||
const baseCtx = {
|
const baseCtx = {
|
||||||
resolveTimeoutFromOptions: () => undefined,
|
resolveTimeoutFromOptions: () => undefined,
|
||||||
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
resolveRelayBaseUrlFromOptions: () => 'https://relay.example',
|
||||||
|
resolveProfileNameFromOptions: () => undefined,
|
||||||
p: () => '',
|
p: () => '',
|
||||||
printTweets: () => undefined,
|
printTweets: () => undefined,
|
||||||
} as unknown as CliContext;
|
} as unknown as CliContext;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ describe('root help output', () => {
|
|||||||
cmd.outputHelp();
|
cmd.outputHelp();
|
||||||
|
|
||||||
expect(help).toContain('--relay-base-url');
|
expect(help).toContain('--relay-base-url');
|
||||||
|
expect(help).toContain('--profile-name');
|
||||||
expect(help).not.toContain('--auth-token');
|
expect(help).not.toContain('--auth-token');
|
||||||
expect(help).not.toContain('--ct0');
|
expect(help).not.toContain('--ct0');
|
||||||
expect(help).toContain('--timeout');
|
expect(help).toContain('--timeout');
|
||||||
|
|||||||
@@ -124,6 +124,41 @@ describe('TwitterClient core coverage', () => {
|
|||||||
expect(headers).toEqual({});
|
expect(headers).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds x-profile-name header when profileName is set', () => {
|
||||||
|
const client = new TwitterClient({ relayBaseUrl: testRelayBaseUrl, profileName: 'work' });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientPrivate;
|
||||||
|
|
||||||
|
expect(clientPrivate.getHeaders()).toEqual({ 'x-profile-name': 'work' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores blank profileName values', () => {
|
||||||
|
const client = new TwitterClient({ relayBaseUrl: testRelayBaseUrl, profileName: ' ' });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientPrivate;
|
||||||
|
|
||||||
|
expect(clientPrivate.getHeaders()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds x-profile-name to JSON headers when profileName is set', () => {
|
||||||
|
const client = new TwitterClient({ relayBaseUrl: testRelayBaseUrl, profileName: 'work' });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientPrivate & {
|
||||||
|
getJsonHeaders: () => Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(clientPrivate.getJsonHeaders()).toEqual({
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-profile-name': 'work',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds x-profile-name to upload headers when profileName is set', () => {
|
||||||
|
const client = new TwitterClient({ relayBaseUrl: testRelayBaseUrl, profileName: 'work' });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientPrivate & {
|
||||||
|
getUploadHeaders: () => Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(clientPrivate.getUploadHeaders()).toEqual({ 'x-profile-name': 'work' });
|
||||||
|
});
|
||||||
|
|
||||||
describe('fetchUserArticlePlainText failures', () => {
|
describe('fetchUserArticlePlainText failures', () => {
|
||||||
it('returns empty object when response is not ok', async () => {
|
it('returns empty object when response is not ok', async () => {
|
||||||
const mockFetch = vi.fn().mockResolvedValueOnce(makeResponse({ ok: false, status: 500 }));
|
const mockFetch = vi.fn().mockResolvedValueOnce(makeResponse({ ok: false, status: 500 }));
|
||||||
|
|||||||
Reference in New Issue
Block a user