import { TWITTER_API_BASE, TWITTER_GRAPHQL_POST_URL, TWITTER_STATUS_UPDATE_URL } from './twitter-client-constants.js'; import { buildTweetCreateFeatures } from './twitter-client-features.js'; export function withPosting(Base) { class TwitterClientPosting extends Base { // biome-ignore lint/complexity/noUselessConstructor lint/suspicious/noExplicitAny: TS mixin constructor requirement. constructor(...args) { super(...args); } /** * Post a new tweet */ async tweet(text, mediaIds) { const variables = { tweet_text: text, dark_request: false, media: { media_entities: (mediaIds ?? []).map((id) => ({ media_id: id, tagged_users: [] })), possibly_sensitive: false, }, semantic_annotation_ids: [], }; const features = buildTweetCreateFeatures(); return this.createTweet(variables, features); } /** * Reply to an existing tweet */ async reply(text, replyToTweetId, mediaIds) { const variables = { tweet_text: text, reply: { in_reply_to_tweet_id: replyToTweetId, exclude_reply_user_ids: [], }, dark_request: false, media: { media_entities: (mediaIds ?? []).map((id) => ({ media_id: id, tagged_users: [] })), possibly_sensitive: false, }, semantic_annotation_ids: [], }; const features = buildTweetCreateFeatures(); return this.createTweet(variables, features); } async createTweet(variables, features) { await this.ensureClientUserId(); let queryId = await this.getQueryId('CreateTweet'); let urlWithOperation = `${TWITTER_API_BASE}/${queryId}/CreateTweet`; const buildBody = () => JSON.stringify({ variables, features, queryId }); let body = buildBody(); try { const headers = { ...this.getHeaders(), referer: 'https://x.com/compose/post' }; let response = await this.fetchWithTimeout(urlWithOperation, { method: 'POST', headers, body, }); // Twitter increasingly prefers POST to /i/api/graphql with queryId in the payload. // If the operation URL 404s, retry the generic endpoint. if (response.status === 404) { await this.refreshQueryIds(); queryId = await this.getQueryId('CreateTweet'); urlWithOperation = `${TWITTER_API_BASE}/${queryId}/CreateTweet`; body = buildBody(); response = await this.fetchWithTimeout(urlWithOperation, { method: 'POST', headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' }, body, }); if (response.status === 404) { const retry = await this.fetchWithTimeout(TWITTER_GRAPHQL_POST_URL, { method: 'POST', headers: { ...this.getHeaders(), referer: 'https://x.com/compose/post' }, body, }); if (!retry.ok) { const text = await retry.text(); return { success: false, error: `HTTP ${retry.status}: ${text.slice(0, 200)}` }; } const data = (await retry.json()); if (data.errors && data.errors.length > 0) { const fallback = await this.tryStatusUpdateFallback(data.errors, variables); if (fallback) { return fallback; } return { success: false, error: this.formatErrors(data.errors) }; } const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id; if (tweetId) { return { success: true, tweetId }; } return { success: false, error: 'Tweet created but no ID returned' }; } } if (!response.ok) { const text = await response.text(); return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}`, }; } const data = (await response.json()); if (data.errors && data.errors.length > 0) { const fallback = await this.tryStatusUpdateFallback(data.errors, variables); if (fallback) { return fallback; } return { success: false, error: this.formatErrors(data.errors), }; } const tweetId = data.data?.create_tweet?.tweet_results?.result?.rest_id; if (tweetId) { return { success: true, tweetId, }; } return { success: false, error: 'Tweet created but no ID returned', }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } formatErrors(errors) { return errors .map((error) => (typeof error.code === 'number' ? `${error.message} (${error.code})` : error.message)) .join(', '); } statusUpdateInputFromCreateTweetVariables(variables) { const text = typeof variables.tweet_text === 'string' ? variables.tweet_text : null; if (!text) { return null; } const reply = variables.reply; const inReplyToTweetId = reply && typeof reply === 'object' && typeof reply.in_reply_to_tweet_id === 'string' ? reply.in_reply_to_tweet_id : undefined; const media = variables.media; const mediaEntities = media && typeof media === 'object' ? media.media_entities : undefined; const mediaIds = Array.isArray(mediaEntities) ? mediaEntities .map((entity) => entity && typeof entity === 'object' && 'media_id' in entity ? entity.media_id : undefined) .filter((value) => typeof value === 'string' || typeof value === 'number') .map((value) => String(value)) : undefined; return { text, inReplyToTweetId, mediaIds: mediaIds && mediaIds.length > 0 ? mediaIds : undefined }; } async postStatusUpdate(input) { const params = new URLSearchParams(); params.set('status', input.text); if (input.inReplyToTweetId) { params.set('in_reply_to_status_id', input.inReplyToTweetId); params.set('auto_populate_reply_metadata', 'true'); } if (input.mediaIds && input.mediaIds.length > 0) { params.set('media_ids', input.mediaIds.join(',')); } try { const response = await this.fetchWithTimeout(TWITTER_STATUS_UPDATE_URL, { method: 'POST', headers: { ...this.getBaseHeaders(), 'content-type': 'application/x-www-form-urlencoded', referer: 'https://x.com/compose/post', }, body: params.toString(), }); if (!response.ok) { const text = await response.text(); return { success: false, error: `HTTP ${response.status}: ${text.slice(0, 200)}` }; } const data = (await response.json()); if (data.errors && data.errors.length > 0) { return { success: false, error: this.formatErrors(data.errors) }; } const tweetId = typeof data.id_str === 'string' ? data.id_str : data.id !== undefined ? String(data.id) : undefined; if (tweetId) { return { success: true, tweetId }; } return { success: false, error: 'Tweet created but no ID returned' }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async tryStatusUpdateFallback(errors, variables) { if (!errors.some((error) => error.code === 226)) { return null; } const input = this.statusUpdateInputFromCreateTweetVariables(variables); if (!input) { return null; } const fallback = await this.postStatusUpdate(input); if (fallback.success) { return fallback; } return { success: false, error: `${this.formatErrors(errors)} | fallback: ${fallback.error ?? 'Unknown error'}`, }; } } return TwitterClientPosting; } //# sourceMappingURL=twitter-client-posting.js.map