build-in-public-bot
Version:
AI-powered CLI bot for automating build-in-public tweets with code screenshots
231 lines • 10.2 kB
JavaScript
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
;