build-in-public-bot
Version:
AI-powered CLI bot for automating build-in-public tweets with code screenshots
459 lines • 20.5 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TwitterService = void 0;
const config_1 = require("./config");
const errors_1 = require("../utils/errors");
const logger_1 = require("../utils/logger");
const twitter_auth_1 = require("./twitter-auth");
const twitter_api_1 = require("./twitter-api");
const path_1 = __importDefault(require("path"));
const promises_1 = __importDefault(require("fs/promises"));
class TwitterService {
static instance;
configService;
authService;
apiClient = null;
authData = null;
constructor() {
this.configService = config_1.ConfigService.getInstance();
this.authService = new twitter_auth_1.TwitterAuthService();
}
static getInstance() {
if (!TwitterService.instance) {
TwitterService.instance = new TwitterService();
}
return TwitterService.instance;
}
async authenticate(username, password) {
try {
this.authData = await this.authService.authenticate(username, password);
this.apiClient = new twitter_api_1.TwitterAPIClient(this.authData);
const authPath = this.getAuthDataPath();
await this.authService.saveAuthData(this.authData, authPath);
const config = await this.configService.load();
config.twitter.username = username;
config.twitter.sessionData = authPath;
await this.configService.save(config);
logger_1.logger.success(`Successfully authenticated as @${username}`);
}
catch (error) {
throw new errors_1.TwitterError('Authentication failed', error);
}
}
async loadSession() {
try {
const config = await this.configService.load();
if (!config.twitter.sessionData) {
return false;
}
const authData = await this.authService.loadAuthData(config.twitter.sessionData);
if (!authData) {
return false;
}
if (authData.savedAt) {
const savedDate = new Date(authData.savedAt);
const daysSince = (Date.now() - savedDate.getTime()) / (1000 * 60 * 60 * 24);
if (daysSince > 30) {
logger_1.logger.warn('Authentication data is older than 30 days. Re-authentication recommended.');
}
}
this.authData = authData;
this.apiClient = new twitter_api_1.TwitterAPIClient(authData);
try {
await this.apiClient.getRateLimitStatus();
return true;
}
catch (error) {
logger_1.logger.warn('Session validation failed. Re-authentication required.');
return false;
}
}
catch (error) {
return false;
}
}
async postTweet(text, mediaIds) {
if (!this.apiClient) {
const loaded = await this.loadSession();
if (!loaded) {
throw new errors_1.TwitterError('Not authenticated. Please run "bip init" first.');
}
}
if (!this.apiClient) {
throw new errors_1.TwitterError('API client not initialized');
}
try {
return await this.apiClient.postTweet(text, mediaIds);
}
catch (error) {
if (error.message?.includes('Authentication expired')) {
this.apiClient = null;
this.authData = null;
}
throw error;
}
}
async uploadMedia(filePath) {
if (!this.apiClient) {
const loaded = await this.loadSession();
if (!loaded) {
throw new errors_1.TwitterError('Not authenticated. Please run "bip init" first.');
}
}
if (!this.apiClient) {
throw new errors_1.TwitterError('API client not initialized');
}
return await this.apiClient.uploadMedia(filePath);
}
async verifyCredentials() {
if (!this.apiClient) {
return false;
}
try {
await this.apiClient.getRateLimitStatus();
return true;
}
catch (error) {
return false;
}
}
async getRateLimitStatus() {
if (!this.apiClient) {
throw new errors_1.TwitterError('Not authenticated');
}
return await this.apiClient.getRateLimitStatus();
}
isAuthenticated() {
return !!this.apiClient;
}
getUsername() {
return this.authData?.username || null;
}
async post(text, media) {
if (!text || text.trim().length === 0) {
throw new errors_1.TwitterError('Tweet text cannot be empty');
}
if (text.length > 280) {
throw new errors_1.TwitterError('Tweet exceeds 280 character limit');
}
const config = await this.configService.load();
const postingMethod = config.twitter.postingMethod || 'browser';
if (postingMethod === 'browser') {
return this.postViaBrowser(text, media);
}
else {
if (!this.apiClient) {
throw new errors_1.TwitterError('Not authenticated. Please run authenticate() first');
}
try {
let mediaId;
if (media) {
const tempPath = path_1.default.join(process.cwd(), '.bip-temp', `temp-${Date.now()}.png`);
await promises_1.default.mkdir(path_1.default.dirname(tempPath), { recursive: true });
await promises_1.default.writeFile(tempPath, media);
try {
mediaId = await this.apiClient.uploadMedia(tempPath);
}
finally {
await promises_1.default.unlink(tempPath).catch(() => { });
}
}
const tweetId = await this.postTweet(text, mediaId ? [mediaId] : undefined);
const username = this.getUsername() || 'user';
return {
id: tweetId,
url: `https://twitter.com/${username}/status/${tweetId}`
};
}
catch (error) {
throw new errors_1.TwitterError('Failed to post tweet', error);
}
}
}
async validateCredentials() {
try {
const authPath = this.getAuthDataPath();
const authData = await this.authService.loadAuthData(authPath);
if (!authData) {
return false;
}
const client = new twitter_api_1.TwitterAPIClient(authData);
try {
await client.getRateLimitStatus();
return true;
}
catch {
return false;
}
}
catch {
return false;
}
}
async logout() {
this.authData = null;
this.apiClient = null;
try {
const authPath = this.getAuthDataPath();
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
await fs.unlink(authPath);
}
catch {
}
}
getAuthDataPath() {
const configDir = this.configService.getConfigDir();
return path_1.default.join(configDir, 'twitter-auth.json');
}
async postViaBrowser(text, media) {
const config = await this.configService.load();
const automationMethod = config.twitter.automationMethod || 'puppeteer';
logger_1.logger.info(`Using ${automationMethod} browser automation to post tweet...`);
if (automationMethod === 'playwright') {
const { TwitterPlaywrightService } = await Promise.resolve().then(() => __importStar(require('./twitter-playwright')));
const playwrightService = new TwitterPlaywrightService();
const username = config.twitter.username;
const password = '';
return await playwrightService.postTweet(username, password, text);
}
else if (automationMethod === 'nodriver') {
const { TwitterNodriverService } = await Promise.resolve().then(() => __importStar(require('./twitter-nodriver')));
const nodriverService = new TwitterNodriverService();
const username = config.twitter.username;
const password = '';
return await nodriverService.postTweet(username, password, text);
}
logger_1.logger.info('Using Puppeteer browser automation to post tweet...');
const hasSession = await this.loadSession();
if (!hasSession) {
logger_1.logger.info('No saved session found. Opening browser for login...');
logger_1.logger.info('Please log in to Twitter in the browser window.');
const username = config.twitter.username;
await this.authenticate(username, '');
}
const browser = await this.authService.launchBrowser(false);
const page = await browser.newPage();
try {
const viewportWidth = 1920 + Math.floor(Math.random() * 100);
const viewportHeight = 1080 + Math.floor(Math.random() * 100);
await page.setViewport({ width: viewportWidth, height: viewportHeight });
if (this.authData?.cookies) {
await page.setCookie(...this.authData.cookies);
}
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
await page.goto('https://twitter.com/home', { waitUntil: 'networkidle2' });
logger_1.logger.debug('Warming up session with natural browsing...');
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 1000));
for (let i = 0; i < 3; i++) {
await page.evaluate(() => {
window.scrollBy({
top: 100 + Math.random() * 300,
behavior: 'smooth'
});
});
await new Promise(resolve => setTimeout(resolve, 500 + Math.random() * 1000));
}
try {
const interactiveElements = await page.$$('[data-testid="like"], [data-testid="retweet"]');
const elementsToHover = Math.min(2 + Math.floor(Math.random() * 3), interactiveElements.length);
for (let i = 0; i < elementsToHover; i++) {
const element = interactiveElements[Math.floor(Math.random() * interactiveElements.length)];
const box = await element.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 + Math.floor(Math.random() * 10) });
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
}
}
}
catch (e) {
}
if (Math.random() < 0.1) {
await page.goto('https://twitter.com/notifications', { waitUntil: 'networkidle2' });
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000));
await page.goto('https://twitter.com/home', { waitUntil: 'networkidle2' });
}
await page.goto('https://twitter.com/compose/tweet', { waitUntil: 'networkidle2' });
const errorText = await page.evaluate(() => {
const bodyText = document.body.innerText;
if (bodyText.includes('Something went wrong') || bodyText.includes('privacy related extensions')) {
return bodyText;
}
return null;
});
if (errorText) {
logger_1.logger.error('Twitter is blocking the request:', errorText);
throw new errors_1.TwitterError('Twitter detected automation. Try disabling browser extensions or using a different browser.');
}
await page.waitForSelector('[data-testid="tweetTextarea_0"]', { timeout: 10000 });
const textarea = await page.$('[data-testid="tweetTextarea_0"]');
await textarea.click();
const words = text.split(' ');
for (let i = 0; i < words.length; i++) {
const word = words[i];
for (const char of word) {
await page.keyboard.type(char);
const baseDelay = 80 + Math.random() * 120;
const pauseChance = Math.random();
let delay = baseDelay;
if (pauseChance < 0.05) {
delay += 300 + Math.random() * 500;
}
await new Promise(resolve => setTimeout(resolve, delay));
}
if (i < words.length - 1) {
await page.keyboard.press('Space');
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 150));
}
if (Math.random() < 0.2) {
const currentMouse = await page.evaluate(() => ({
x: 400 + Math.random() * 800,
y: 200 + Math.random() * 400
}));
await page.mouse.move(currentMouse.x + (Math.random() - 0.5) * 100, currentMouse.y + (Math.random() - 0.5) * 50, { steps: Math.floor(5 + Math.random() * 10) });
}
}
if (media) {
const tempPath = path_1.default.join(process.cwd(), '.bip-temp', `temp-${Date.now()}.png`);
await promises_1.default.mkdir(path_1.default.dirname(tempPath), { recursive: true });
await promises_1.default.writeFile(tempPath, media);
const fileInput = await page.$('input[type="file"]');
if (fileInput) {
await fileInput.uploadFile(tempPath);
await page.waitForTimeout(3000);
}
await promises_1.default.unlink(tempPath).catch(() => { });
}
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 2000));
logger_1.logger.debug('Moving to tweet button...');
const tweetButton = await page.$('[data-testid="tweetButtonInline"]');
const box = await tweetButton.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2, { steps: 10 });
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
await page.mouse.click(box.x + box.width / 2 + (Math.random() - 0.5) * 10, box.y + box.height / 2 + (Math.random() - 0.5) * 10);
}
else {
await page.click('[data-testid="tweetButtonInline"]');
}
logger_1.logger.debug('Waiting for tweet to be posted...');
await new Promise(resolve => setTimeout(resolve, 2000));
const username = this.getUsername() || 'user';
let tweetId = null;
let currentUrl = page.url();
logger_1.logger.debug(`Current URL after posting: ${currentUrl}`);
let tweetIdMatch = currentUrl.match(/status\/(\d+)/);
if (tweetIdMatch && tweetIdMatch[1]) {
tweetId = tweetIdMatch[1];
logger_1.logger.debug(`Tweet ID extracted from immediate URL: ${tweetId}`);
}
if (!tweetId) {
try {
await page.waitForFunction(() => window.location.href.includes('/status/'), { timeout: 8000 });
currentUrl = page.url();
tweetIdMatch = currentUrl.match(/status\/(\d+)/);
if (tweetIdMatch && tweetIdMatch[1]) {
tweetId = tweetIdMatch[1];
logger_1.logger.debug(`Tweet ID extracted after URL change: ${tweetId}`);
}
}
catch (waitError) {
logger_1.logger.debug('URL change detection timed out');
}
}
if (!tweetId) {
try {
await page.waitForSelector('[data-testid="tweet"]', { timeout: 5000 });
const tweetLinks = await page.$$eval('a[href*="/status/"]', links => links.map(link => link.getAttribute('href')).filter(href => href));
for (const link of tweetLinks) {
if (link) {
const match = link.match(/\/status\/(\d+)/);
if (match && match[1]) {
tweetId = match[1];
logger_1.logger.debug(`Tweet ID extracted from page element: ${tweetId}`);
break;
}
}
}
}
catch (elementError) {
logger_1.logger.debug('Could not find tweet elements on page');
}
}
if (!tweetId) {
try {
const history = await page.evaluate(() => {
return window.history.length > 1 ? document.referrer : window.location.href;
});
const historyMatch = history.match(/status\/(\d+)/);
if (historyMatch && historyMatch[1]) {
tweetId = historyMatch[1];
logger_1.logger.debug(`Tweet ID extracted from browser history: ${tweetId}`);
}
}
catch (historyError) {
logger_1.logger.debug('Could not access browser history');
}
}
if (tweetId) {
return {
id: tweetId,
url: `https://twitter.com/${username}/status/${tweetId}`
};
}
else {
const timestamp = Date.now();
const randomBits = Math.floor(Math.random() * 1000000);
const fallbackId = `${timestamp}${randomBits}`;
logger_1.logger.warn('Could not extract tweet ID from any method, using generated fallback');
return {
id: fallbackId,
url: `https://twitter.com/${username}/status/${fallbackId}`
};
}
}
catch (error) {
throw new errors_1.TwitterError('Failed to post tweet via browser', error);
}
finally {
await browser.close();
}
}
}
exports.TwitterService = TwitterService;
//# sourceMappingURL=twitter.js.map
;