diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9b97b..64af7d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.4.2 — Unreleased +## 0.5.0 — 2026-01-01 ### Added - `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). - Search now paginates beyond 20 results when using `-n` (thanks @ryanh-ai). - Library exports are now separated from the CLI entrypoint for easier embedding. + ## 0.4.1 — 2025-12-31 ### Added diff --git a/package.json b/package.json index feedfc1..0ff8fd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@steipete/bird", - "version": "0.4.1", + "version": "0.5.0", "description": "CLI tool for tweeting and replying via Twitter/X GraphQL API", "type": "module", "main": "dist/index.js", diff --git a/src/lib/twitter-client-base.ts b/src/lib/twitter-client-base.ts index fed9250..9b9f7c8 100644 --- a/src/lib/twitter-client-base.ts +++ b/src/lib/twitter-client-base.ts @@ -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 { normalizeQuoteDepth } from './twitter-client-utils.js'; -export type Constructor = new (...args: unknown[]) => T; -export type AbstractConstructor = abstract new (...args: unknown[]) => T; +// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement. +export type Constructor = new (...args: any[]) => T; +// biome-ignore lint/suspicious/noExplicitAny: TS mixin base constructor requirement. +export type AbstractConstructor = abstract new (...args: any[]) => T; +export type Mixin, TAdded> = abstract new ( + ...args: ConstructorParameters +) => TwitterClientBase & TAdded; export abstract class TwitterClientBase { protected authToken: string; diff --git a/src/lib/twitter-client-media.ts b/src/lib/twitter-client-media.ts index ef3e0e4..bb2afcd 100644 --- a/src/lib/twitter-client-media.ts +++ b/src/lib/twitter-client-media.ts @@ -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 type { UploadMediaResult } from './twitter-client-types.js'; -export function withMedia>(Base: TBase) { - return class extends Base { +export interface TwitterClientMediaMethods { + uploadMedia(input: { data: Uint8Array; mimeType: string; alt?: string }): Promise; +} + +export function withMedia>( + Base: TBase, +): Mixin { + 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 { if (mimeType.startsWith('image/')) { if (mimeType === 'image/gif') { @@ -162,5 +173,7 @@ export function withMedia>( return { success: false, error: error instanceof Error ? error.message : String(error) }; } } - }; + } + + return TwitterClientMedia; } diff --git a/src/lib/twitter-client-posting.ts b/src/lib/twitter-client-posting.ts index c64d8d9..693ffe6 100644 --- a/src/lib/twitter-client-posting.ts +++ b/src/lib/twitter-client-posting.ts @@ -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 { buildTweetCreateFeatures } from './twitter-client-features.js'; import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js'; -export function withPosting>(Base: TBase) { - return class extends Base { +export interface TwitterClientPostingMethods { + tweet(text: string, mediaIds?: string[]): Promise; + reply(text: string, replyToTweetId: string, mediaIds?: string[]): Promise; +} + +export function withPosting>( + Base: TBase, +): Mixin { + 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 */ @@ -269,5 +281,7 @@ export function withPosting error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`, }; } - }; + } + + return TwitterClientPosting; } diff --git a/src/lib/twitter-client-search.ts b/src/lib/twitter-client-search.ts index df122c3..d7ae437 100644 --- a/src/lib/twitter-client-search.ts +++ b/src/lib/twitter-client-search.ts @@ -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 { buildSearchFeatures } from './twitter-client-features.js'; import type { SearchResult, TweetData } from './twitter-client-types.js'; import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js'; -export function withSearch>(Base: TBase) { - return class extends Base { +export interface TwitterClientSearchMethods { + search(query: string, count?: number): Promise; +} + +export function withSearch>( + Base: TBase, +): Mixin { + 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 */ @@ -156,5 +167,7 @@ export function withSearch> return { success: true, tweets }; } - }; + } + + return TwitterClientSearch; } diff --git a/src/lib/twitter-client-timelines.ts b/src/lib/twitter-client-timelines.ts index a29ad51..1cc7bb2 100644 --- a/src/lib/twitter-client-timelines.ts +++ b/src/lib/twitter-client-timelines.ts @@ -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 { 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'; -export function withTimelines>(Base: TBase) { - return class extends Base { +export interface TwitterClientTimelineMethods { + getBookmarks(count?: number): Promise; + getLikes(count?: number): Promise; + getBookmarkFolderTimeline(folderId: string, count?: number): Promise; +} + +export function withTimelines>( + Base: TBase, +): Mixin { + 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 { const primary = await this.getQueryId('Bookmarks'); return Array.from(new Set([primary, 'RV1g3b8n_SGOHwkqKYSCFw', 'tmd4ifV8RHltzn8ymGg1aw'])); @@ -74,7 +87,7 @@ export function withTimelines>(Base: TBase) { - return class extends Base { +export interface TwitterClientTweetDetailMethods { + getTweet(tweetId: string): Promise; + getReplies(tweetId: string): Promise; + getThread(tweetId: string): Promise; +} + +export function withTweetDetails>( + Base: TBase, +): Mixin { + 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( userId: string, tweetId: string, @@ -318,5 +331,7 @@ export function withTweetDetails>(Base: TBase) { - return class extends Base { +export interface TwitterClientUserMethods { + getCurrentUser(): Promise; + getFollowing(userId: string, count?: number): Promise; + getFollowers(userId: string, count?: number): Promise; +} + +export function withUsers>( + Base: TBase, +): Mixin { + 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 { const primary = await this.getQueryId('Following'); return Array.from(new Set([primary, 'BEkNpEt5pNETESoqMsTEGA'])); @@ -326,5 +339,7 @@ export function withUsers>( return { success: false, error: firstAttempt.error }; } - }; + } + + return TwitterClientUsers; } diff --git a/src/lib/twitter-client.ts b/src/lib/twitter-client.ts index 8acea24..70275fc 100644 --- a/src/lib/twitter-client.ts +++ b/src/lib/twitter-client.ts @@ -1,14 +1,23 @@ +import type { AbstractConstructor } from './twitter-client-base.js'; import { TwitterClientBase } from './twitter-client-base.js'; -import { withMedia } from './twitter-client-media.js'; -import { withPosting } from './twitter-client-posting.js'; -import { withSearch } from './twitter-client-search.js'; -import { withTimelines } from './twitter-client-timelines.js'; -import { withTweetDetails } from './twitter-client-tweet-detail.js'; -import { withUsers } from './twitter-client-users.js'; +import { type TwitterClientMediaMethods, withMedia } from './twitter-client-media.js'; +import { type TwitterClientPostingMethods, withPosting } from './twitter-client-posting.js'; +import { type TwitterClientSearchMethods, withSearch } from './twitter-client-search.js'; +import { type TwitterClientTimelineMethods, withTimelines } from './twitter-client-timelines.js'; +import { type TwitterClientTweetDetailMethods, withTweetDetails } from './twitter-client-tweet-detail.js'; +import { type TwitterClientUserMethods, withUsers } from './twitter-client-users.js'; + +type TwitterClientInstance = TwitterClientBase & + TwitterClientMediaMethods & + TwitterClientPostingMethods & + TwitterClientSearchMethods & + TwitterClientTimelineMethods & + TwitterClientTweetDetailMethods & + TwitterClientUserMethods; const MixedTwitterClient = withUsers( withTimelines(withSearch(withTweetDetails(withPosting(withMedia(TwitterClientBase))))), -); +) as AbstractConstructor; export class TwitterClient extends MixedTwitterClient {}