UNPKG

build-in-public-bot

Version:

AI-powered CLI bot for automating build-in-public tweets with code screenshots

231 lines 10.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TwitterAPIClient = void 0; const axios_1 = __importDefault(require("axios")); const errors_1 = require("../utils/errors"); const logger_1 = require("../utils/logger"); const form_data_1 = __importDefault(require("form-data")); const promises_1 = __importDefault(require("fs/promises")); class TwitterAPIClient { client; graphqlFeatures = { "rweb_lists_timeline_redesign_enabled": true, "responsive_web_graphql_exclude_directive_enabled": true, "verified_phone_label_enabled": false, "creator_subscriptions_tweet_preview_api_enabled": true, "responsive_web_graphql_timeline_navigation_enabled": true, "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, "tweetypie_unmention_optimization_enabled": true, "responsive_web_edit_tweet_api_enabled": true, "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, "view_counts_everywhere_api_enabled": true, "longform_notetweets_consumption_enabled": true, "responsive_web_twitter_article_tweet_consumption_enabled": true, "tweet_awards_web_tipping_enabled": false, "freedom_of_speech_not_reach_fetch_enabled": true, "standardized_nudges_misinfo": true, "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": false, "longform_notetweets_rich_text_read_enabled": true, "longform_notetweets_inline_media_enabled": true, "responsive_web_media_download_video_enabled": false, "responsive_web_enhance_cards_enabled": false }; constructor(authData) { const cookieString = authData.cookies .map(cookie => `${cookie.name}=${cookie.value}`) .join('; '); this.client = axios_1.default.create({ baseURL: 'https://twitter.com', headers: { 'authority': 'twitter.com', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.9', 'authorization': `Bearer ${process.env.TWITTER_BEARER_TOKEN || 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'}`, 'content-type': 'application/json', 'cookie': cookieString, 'origin': 'https://twitter.com', 'referer': 'https://twitter.com/compose/tweet', 'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'x-csrf-token': authData.ct0 || '', 'x-twitter-active-user': 'yes', 'x-twitter-auth-type': 'OAuth2Session', 'x-twitter-client-language': 'en' }, timeout: 30000 }); this.client.interceptors.response.use(response => response, this.handleApiError.bind(this)); } async postTweet(text, mediaIds) { try { logger_1.logger.debug('Preparing tweet for posting...'); const variables = { tweet_text: text, dark_request: false, media: mediaIds ? { media_entities: mediaIds.map(id => ({ media_id: id, tagged_users: [] })), possibly_sensitive: false } : undefined, semantic_annotation_ids: [] }; const response = await this.client.post('/i/api/graphql/SoVnbfCycZ7fERGCwpZkYA/CreateTweet', { variables, features: this.graphqlFeatures, queryId: 'SoVnbfCycZ7fERGCwpZkYA' }); const tweetId = response.data.data.create_tweet.tweet_results.result.rest_id; logger_1.logger.debug(`Tweet posted successfully with ID: ${tweetId}`); return tweetId; } catch (error) { throw this.handleTweetError(error); } } async uploadMedia(filePath) { try { logger_1.logger.debug(`Uploading media from: ${filePath}`); const fileData = await promises_1.default.readFile(filePath); const totalBytes = fileData.length; const initResponse = await this.uploadInit(totalBytes, 'image/png'); const mediaId = initResponse.media_id_string; await this.uploadAppend(mediaId, fileData, 0); await this.uploadFinalize(mediaId); await this.checkUploadStatus(mediaId); logger_1.logger.debug(`Media uploaded successfully. ID: ${mediaId}`); return mediaId; } catch (error) { throw this.handleMediaError(error); } } async uploadInit(totalBytes, mediaType) { const params = new URLSearchParams({ command: 'INIT', total_bytes: totalBytes.toString(), media_type: mediaType, media_category: 'tweet_image' }); const response = await this.client.post('https://upload.twitter.com/1.1/media/upload.json', params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.data; } async uploadAppend(mediaId, data, segmentIndex) { const form = new form_data_1.default(); form.append('command', 'APPEND'); form.append('media_id', mediaId); form.append('segment_index', segmentIndex.toString()); form.append('media', data, 'media.png'); await this.client.post('https://upload.twitter.com/1.1/media/upload.json', form, { headers: { ...form.getHeaders() } }); } async uploadFinalize(mediaId) { const params = new URLSearchParams({ command: 'FINALIZE', media_id: mediaId }); await this.client.post('https://upload.twitter.com/1.1/media/upload.json', params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); } async checkUploadStatus(mediaId) { let attempts = 0; const maxAttempts = 10; while (attempts < maxAttempts) { const params = new URLSearchParams({ command: 'STATUS', media_id: mediaId }); const response = await this.client.get('https://upload.twitter.com/1.1/media/upload.json?' + params.toString()); const { processing_info } = response.data; if (!processing_info || processing_info.state === 'succeeded') { return; } if (processing_info.state === 'failed') { throw new errors_1.TwitterError(`Media processing failed: ${processing_info.error?.message || 'Unknown error'}`); } await new Promise(resolve => setTimeout(resolve, processing_info.check_after_secs * 1000 || 1000)); attempts++; } throw new errors_1.TwitterError('Media processing timeout'); } async getRateLimitStatus() { try { const response = await this.client.get('/i/api/1.1/application/rate_limit_status.json', { params: { resources: 'statuses' } }); const limits = response.data.resources.statuses['/statuses/update']; return { remaining: limits.remaining, limit: limits.limit, reset: new Date(limits.reset * 1000) }; } catch (error) { return { remaining: 50, limit: 50, reset: new Date(Date.now() + 15 * 60 * 1000) }; } } handleApiError(error) { if (error.response) { const status = error.response.status; const data = error.response.data; if (status === 401) { throw new errors_1.TwitterError('Authentication expired. Please re-authenticate.'); } else if (status === 403) { throw new errors_1.TwitterError('Access forbidden. Your account may be restricted.'); } else if (status === 429) { const resetTime = error.response.headers['x-rate-limit-reset']; const reset = resetTime ? new Date(parseInt(resetTime) * 1000) : new Date(); throw new errors_1.TwitterError(`Rate limit exceeded. Try again after ${reset.toLocaleTimeString()}`); } else if (data?.errors) { const errorMessages = data.errors.map((e) => e.message).join(', '); throw new errors_1.TwitterError(`Twitter API error: ${errorMessages}`); } } throw error; } handleTweetError(error) { if (error instanceof errors_1.TwitterError) { return error; } if (error.response?.data?.errors) { const errors = error.response.data.errors; if (errors.some((e) => e.message?.includes('duplicate'))) { return new errors_1.TwitterError('This tweet is a duplicate of a recent tweet.'); } } return new errors_1.TwitterError(`Failed to post tweet: ${error.message}`, error); } handleMediaError(error) { if (error instanceof errors_1.TwitterError) { return error; } return new errors_1.TwitterError(`Failed to upload media: ${error.message}`, error); } } exports.TwitterAPIClient = TwitterAPIClient; //# sourceMappingURL=twitter-api.js.map