UNPKG

build-in-public-bot

Version:

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

459 lines 20.5 kB
"use strict"; 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