chore(release): 0.5.0
This commit is contained in:
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.4.2 — Unreleased
|
## 0.5.0 — 2026-01-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- `likes` command to list your liked tweets (thanks @swairshah).
|
- `likes` command to list your liked tweets (thanks @swairshah).
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
- Cookie extraction timeout is now configurable (default 30s on macOS) via `--cookie-timeout` / `BIRD_COOKIE_TIMEOUT_MS` (thanks @tylerseymour).
|
- Cookie extraction timeout is now configurable (default 30s on macOS) via `--cookie-timeout` / `BIRD_COOKIE_TIMEOUT_MS` (thanks @tylerseymour).
|
||||||
- Search now paginates beyond 20 results when using `-n` (thanks @ryanh-ai).
|
- Search now paginates beyond 20 results when using `-n` (thanks @ryanh-ai).
|
||||||
- Library exports are now separated from the CLI entrypoint for easier embedding.
|
- Library exports are now separated from the CLI entrypoint for easier embedding.
|
||||||
|
|
||||||
## 0.4.1 — 2025-12-31
|
## 0.4.1 — 2025-12-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@steipete/bird",
|
"name": "@steipete/bird",
|
||||||
"version": "0.4.1",
|
"version": "0.5.0",
|
||||||
"description": "CLI tool for tweeting and replying via Twitter/X GraphQL API",
|
"description": "CLI tool for tweeting and replying via Twitter/X GraphQL API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ import { type OperationName, QUERY_IDS, TARGET_QUERY_ID_OPERATIONS } from './twi
|
|||||||
import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js';
|
import type { CurrentUserResult, TwitterClientOptions } from './twitter-client-types.js';
|
||||||
import { normalizeQuoteDepth } from './twitter-client-utils.js';
|
import { normalizeQuoteDepth } from './twitter-client-utils.js';
|
||||||
|
|
||||||
export type Constructor<T = object> = new (...args: unknown[]) => T;
|
// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
|
||||||
export type AbstractConstructor<T = object> = abstract new (...args: unknown[]) => T;
|
export type Constructor<T = object> = new (...args: any[]) => T;
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement.
|
||||||
|
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;
|
||||||
|
export type Mixin<TBase extends AbstractConstructor<TwitterClientBase>, TAdded> = abstract new (
|
||||||
|
...args: ConstructorParameters<TBase>
|
||||||
|
) => TwitterClientBase & TAdded;
|
||||||
|
|
||||||
export abstract class TwitterClientBase {
|
export abstract class TwitterClientBase {
|
||||||
protected authToken: string;
|
protected authToken: string;
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
|
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||||
import { TWITTER_MEDIA_METADATA_URL, TWITTER_UPLOAD_URL } from './twitter-client-constants.js';
|
import { TWITTER_MEDIA_METADATA_URL, TWITTER_UPLOAD_URL } from './twitter-client-constants.js';
|
||||||
import type { UploadMediaResult } from './twitter-client-types.js';
|
import type { UploadMediaResult } from './twitter-client-types.js';
|
||||||
|
|
||||||
export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
|
export interface TwitterClientMediaMethods {
|
||||||
return class extends Base {
|
uploadMedia(input: { data: Uint8Array; mimeType: string; alt?: string }): Promise<UploadMediaResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||||
|
Base: TBase,
|
||||||
|
): Mixin<TBase, TwitterClientMediaMethods> {
|
||||||
|
abstract class TwitterClientMedia extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
private mediaCategoryForMime(mimeType: string): string | null {
|
private mediaCategoryForMime(mimeType: string): string | null {
|
||||||
if (mimeType.startsWith('image/')) {
|
if (mimeType.startsWith('image/')) {
|
||||||
if (mimeType === 'image/gif') {
|
if (mimeType === 'image/gif') {
|
||||||
@@ -162,5 +173,7 @@ export function withMedia<TBase extends AbstractConstructor<TwitterClientBase>>(
|
|||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TwitterClientMedia;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
|
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||||
import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js';
|
import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js';
|
||||||
import { buildTweetCreateFeatures } from './twitter-client-features.js';
|
import { buildTweetCreateFeatures } from './twitter-client-features.js';
|
||||||
import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js';
|
import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js';
|
||||||
|
|
||||||
export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
|
export interface TwitterClientPostingMethods {
|
||||||
return class extends Base {
|
tweet(text: string, mediaIds?: string[]): Promise<TweetResult>;
|
||||||
|
reply(text: string, replyToTweetId: string, mediaIds?: string[]): Promise<TweetResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||||
|
Base: TBase,
|
||||||
|
): Mixin<TBase, TwitterClientPostingMethods> {
|
||||||
|
abstract class TwitterClientPosting extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post a new tweet
|
* Post a new tweet
|
||||||
*/
|
*/
|
||||||
@@ -269,5 +281,7 @@ export function withPosting<TBase extends AbstractConstructor<TwitterClientBase>
|
|||||||
error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`,
|
error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TwitterClientPosting;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
|
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||||
import { buildSearchFeatures } from './twitter-client-features.js';
|
import { buildSearchFeatures } from './twitter-client-features.js';
|
||||||
import type { SearchResult, TweetData } from './twitter-client-types.js';
|
import type { SearchResult, TweetData } from './twitter-client-types.js';
|
||||||
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
|
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
|
||||||
|
|
||||||
export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
|
export interface TwitterClientSearchMethods {
|
||||||
return class extends Base {
|
search(query: string, count?: number): Promise<SearchResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||||
|
Base: TBase,
|
||||||
|
): Mixin<TBase, TwitterClientSearchMethods> {
|
||||||
|
abstract class TwitterClientSearch extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for tweets matching a query
|
* Search for tweets matching a query
|
||||||
*/
|
*/
|
||||||
@@ -156,5 +167,7 @@ export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>
|
|||||||
|
|
||||||
return { success: true, tweets };
|
return { success: true, tweets };
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TwitterClientSearch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
|
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||||
import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-features.js';
|
import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-features.js';
|
||||||
import type { SearchResult } from './twitter-client-types.js';
|
import type { GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
|
||||||
import { parseTweetsFromInstructions } from './twitter-client-utils.js';
|
import { parseTweetsFromInstructions } from './twitter-client-utils.js';
|
||||||
|
|
||||||
export function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
|
export interface TwitterClientTimelineMethods {
|
||||||
return class extends Base {
|
getBookmarks(count?: number): Promise<SearchResult>;
|
||||||
|
getLikes(count?: number): Promise<SearchResult>;
|
||||||
|
getBookmarkFolderTimeline(folderId: string, count?: number): Promise<SearchResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||||
|
Base: TBase,
|
||||||
|
): Mixin<TBase, TwitterClientTimelineMethods> {
|
||||||
|
abstract class TwitterClientTimelines extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
private async getBookmarksQueryIds(): Promise<string[]> {
|
private async getBookmarksQueryIds(): Promise<string[]> {
|
||||||
const primary = await this.getQueryId('Bookmarks');
|
const primary = await this.getQueryId('Bookmarks');
|
||||||
return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw']));
|
return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw']));
|
||||||
@@ -74,7 +87,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
|
|||||||
content?: {
|
content?: {
|
||||||
itemContent?: {
|
itemContent?: {
|
||||||
tweet_results?: {
|
tweet_results?: {
|
||||||
result?: unknown;
|
result?: GraphqlTweetResult;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -180,7 +193,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
|
|||||||
content?: {
|
content?: {
|
||||||
itemContent?: {
|
itemContent?: {
|
||||||
tweet_results?: {
|
tweet_results?: {
|
||||||
result?: unknown;
|
result?: GraphqlTweetResult;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -283,7 +296,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
|
|||||||
content?: {
|
content?: {
|
||||||
itemContent?: {
|
itemContent?: {
|
||||||
tweet_results?: {
|
tweet_results?: {
|
||||||
result?: unknown;
|
result?: GraphqlTweetResult;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -333,5 +346,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
|
|||||||
|
|
||||||
return { success: false, error: firstAttempt.error };
|
return { success: false, error: firstAttempt.error };
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TwitterClientTimelines;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
|
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||||
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
import { TWITTER_API_BASE } from './twitter-client-constants.js';
|
||||||
import { buildArticleFeatures, buildArticleFieldToggles, buildTweetDetailFeatures } from './twitter-client-features.js';
|
import { buildArticleFeatures, buildArticleFieldToggles, buildTweetDetailFeatures } from './twitter-client-features.js';
|
||||||
import type { GetTweetResult, GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
|
import type { GetTweetResult, GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
|
||||||
@@ -10,8 +10,21 @@ import {
|
|||||||
parseTweetsFromInstructions,
|
parseTweetsFromInstructions,
|
||||||
} from './twitter-client-utils.js';
|
} from './twitter-client-utils.js';
|
||||||
|
|
||||||
export function withTweetDetails<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
|
export interface TwitterClientTweetDetailMethods {
|
||||||
return class extends Base {
|
getTweet(tweetId: string): Promise<GetTweetResult>;
|
||||||
|
getReplies(tweetId: string): Promise<SearchResult>;
|
||||||
|
getThread(tweetId: string): Promise<SearchResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withTweetDetails<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||||
|
Base: TBase,
|
||||||
|
): Mixin<TBase, TwitterClientTweetDetailMethods> {
|
||||||
|
abstract class TwitterClientTweetDetails extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
private async fetchUserArticlePlainText(
|
private async fetchUserArticlePlainText(
|
||||||
userId: string,
|
userId: string,
|
||||||
tweetId: string,
|
tweetId: string,
|
||||||
@@ -318,5 +331,7 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
|
|||||||
|
|
||||||
return { success: true, tweets: thread };
|
return { success: true, tweets: thread };
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TwitterClientTweetDetails;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AbstractConstructor, TwitterClientBase } from './twitter-client-base.js';
|
import type { AbstractConstructor, Mixin, TwitterClientBase } from './twitter-client-base.js';
|
||||||
import {
|
import {
|
||||||
SETTINGS_NAME_REGEX,
|
SETTINGS_NAME_REGEX,
|
||||||
SETTINGS_SCREEN_NAME_REGEX,
|
SETTINGS_SCREEN_NAME_REGEX,
|
||||||
@@ -9,8 +9,21 @@ import { buildFollowingFeatures } from './twitter-client-features.js';
|
|||||||
import type { CurrentUserResult, FollowingResult } from './twitter-client-types.js';
|
import type { CurrentUserResult, FollowingResult } from './twitter-client-types.js';
|
||||||
import { parseUsersFromInstructions } from './twitter-client-utils.js';
|
import { parseUsersFromInstructions } from './twitter-client-utils.js';
|
||||||
|
|
||||||
export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(Base: TBase) {
|
export interface TwitterClientUserMethods {
|
||||||
return class extends Base {
|
getCurrentUser(): Promise<CurrentUserResult>;
|
||||||
|
getFollowing(userId: string, count?: number): Promise<FollowingResult>;
|
||||||
|
getFollowers(userId: string, count?: number): Promise<FollowingResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
|
||||||
|
Base: TBase,
|
||||||
|
): Mixin<TBase, TwitterClientUserMethods> {
|
||||||
|
abstract class TwitterClientUsers extends Base {
|
||||||
|
// biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement.
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
private async getFollowingQueryIds(): Promise<string[]> {
|
private async getFollowingQueryIds(): Promise<string[]> {
|
||||||
const primary = await this.getQueryId('Following');
|
const primary = await this.getQueryId('Following');
|
||||||
return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA']));
|
return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA']));
|
||||||
@@ -326,5 +339,7 @@ export function withUsers<TBase extends AbstractConstructor<TwitterClientBase>>(
|
|||||||
|
|
||||||
return { success: false, error: firstAttempt.error };
|
return { success: false, error: firstAttempt.error };
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return TwitterClientUsers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
import type { AbstractConstructor } from './twitter-client-base.js';
|
||||||
import { TwitterClientBase } from './twitter-client-base.js';
|
import { TwitterClientBase } from './twitter-client-base.js';
|
||||||
import { withMedia } from './twitter-client-media.js';
|
import { type TwitterClientMediaMethods, withMedia } from './twitter-client-media.js';
|
||||||
import { withPosting } from './twitter-client-posting.js';
|
import { type TwitterClientPostingMethods, withPosting } from './twitter-client-posting.js';
|
||||||
import { withSearch } from './twitter-client-search.js';
|
import { type TwitterClientSearchMethods, withSearch } from './twitter-client-search.js';
|
||||||
import { withTimelines } from './twitter-client-timelines.js';
|
import { type TwitterClientTimelineMethods, withTimelines } from './twitter-client-timelines.js';
|
||||||
import { withTweetDetails } from './twitter-client-tweet-detail.js';
|
import { type TwitterClientTweetDetailMethods, withTweetDetails } from './twitter-client-tweet-detail.js';
|
||||||
import { withUsers } from './twitter-client-users.js';
|
import { type TwitterClientUserMethods, withUsers } from './twitter-client-users.js';
|
||||||
|
|
||||||
|
type TwitterClientInstance = TwitterClientBase &
|
||||||
|
TwitterClientMediaMethods &
|
||||||
|
TwitterClientPostingMethods &
|
||||||
|
TwitterClientSearchMethods &
|
||||||
|
TwitterClientTimelineMethods &
|
||||||
|
TwitterClientTweetDetailMethods &
|
||||||
|
TwitterClientUserMethods;
|
||||||
|
|
||||||
const MixedTwitterClient = withUsers(
|
const MixedTwitterClient = withUsers(
|
||||||
withTimelines(withSearch(withTweetDetails(withPosting(withMedia(TwitterClientBase))))),
|
withTimelines(withSearch(withTweetDetails(withPosting(withMedia(TwitterClientBase))))),
|
||||||
);
|
) as AbstractConstructor<TwitterClientInstance>;
|
||||||
|
|
||||||
export class TwitterClient extends MixedTwitterClient {}
|
export class TwitterClient extends MixedTwitterClient {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user