← back to Dear Bubbe Nextjs

archived/social-posters/x-graphql-poster.js

252 lines

const https = require('https');
const fs = require('fs').promises;
const path = require('path');

// X.com Internal GraphQL Configuration
// You need to extract these from browser once:
// 1. Open X.com in browser, login
// 2. Open DevTools > Network
// 3. Post a tweet
// 4. Find CreateTweet request
// 5. Copy auth_token from cookies and Bearer token from headers

const X_CONFIG = {
    // These need to be extracted from logged-in session
    auth_token: 'YOUR_AUTH_TOKEN_HERE', // from cookies
    ct0: 'YOUR_CT0_TOKEN_HERE', // from cookies
    bearer: 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', // standard bearer
    
    // GraphQL endpoint for CreateTweet
    graphql_endpoint: '/i/api/graphql/oB-5XsHNAbjvARJEc8CZFw/CreateTweet',
    
    // User agent to match browser
    user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
};

// Load saved tokens if available
async function loadTokens() {
    try {
        const tokensPath = path.join(__dirname, 'x-tokens.json');
        const tokens = JSON.parse(await fs.readFile(tokensPath, 'utf8'));
        Object.assign(X_CONFIG, tokens);
        console.log('✅ Loaded saved X tokens');
        return true;
    } catch (e) {
        console.log('⚠️ No saved tokens found - using defaults');
        return false;
    }
}

// Save tokens for future use
async function saveTokens(tokens) {
    const tokensPath = path.join(__dirname, 'x-tokens.json');
    await fs.writeFile(tokensPath, JSON.stringify(tokens, null, 2));
    console.log('💾 Tokens saved');
}

// Post tweet using GraphQL API
async function postTweet(text) {
    console.log('\n🐦 POSTING TO X.COM via GraphQL');
    console.log('Tweet:', text.substring(0, 100) + '...');
    
    // Load tokens
    await loadTokens();
    
    // Check if we have valid tokens
    if (X_CONFIG.auth_token === 'YOUR_AUTH_TOKEN_HERE') {
        console.error('❌ ERROR: You need to set auth_token and ct0 first!');
        console.log('\nTo get tokens:');
        console.log('1. Open X.com in browser and login');
        console.log('2. Open DevTools > Application > Cookies');
        console.log('3. Find auth_token and ct0 values');
        console.log('4. Update x-tokens.json with these values');
        return false;
    }
    
    return new Promise((resolve, reject) => {
        const postData = JSON.stringify({
            variables: {
                tweet_text: text,
                dark_request: false,
                media: {
                    media_entities: [],
                    possibly_sensitive: false
                },
                semantic_annotation_ids: []
            },
            features: {
                communities_web_enable_tweet_community_results_fetch: true,
                c9s_tweet_anatomy_moderator_badge_enabled: true,
                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,
                creator_subscriptions_quote_tweet_preview_enabled: false,
                longform_notetweets_rich_text_read_enabled: true,
                longform_notetweets_inline_media_enabled: true,
                articles_preview_enabled: true,
                rweb_video_timestamps_enabled: true,
                rweb_tipjar_consumption_enabled: true,
                responsive_web_graphql_exclude_directive_enabled: true,
                verified_phone_label_enabled: false,
                freedom_of_speech_not_reach_fetch_enabled: true,
                standardized_nudges_misinfo: true,
                tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
                responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
                responsive_web_graphql_timeline_navigation_enabled: true,
                responsive_web_enhance_cards_enabled: false
            },
            queryId: 'oB-5XsHNAbjvARJEc8CZFw'
        });
        
        const options = {
            hostname: 'x.com',
            path: X_CONFIG.graphql_endpoint,
            method: 'POST',
            headers: {
                'Host': 'x.com',
                'User-Agent': X_CONFIG.user_agent,
                'Accept': '*/*',
                'Accept-Language': 'en-US,en;q=0.9',
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(postData),
                'authorization': `Bearer ${X_CONFIG.bearer}`,
                'x-csrf-token': X_CONFIG.ct0,
                'x-twitter-auth-type': 'OAuth2Session',
                'x-twitter-client-language': 'en',
                'x-twitter-active-user': 'yes',
                'Cookie': `auth_token=${X_CONFIG.auth_token}; ct0=${X_CONFIG.ct0}; guest_id=v1%3A172000000000000000`,
                'Origin': 'https://x.com',
                'Referer': 'https://x.com/home',
                'Sec-Fetch-Dest': 'empty',
                'Sec-Fetch-Mode': 'cors',
                'Sec-Fetch-Site': 'same-origin'
            }
        };
        
        const req = https.request(options, (res) => {
            let data = '';
            
            res.on('data', (chunk) => {
                data += chunk;
            });
            
            res.on('end', () => {
                try {
                    const response = JSON.parse(data);
                    
                    if (res.statusCode === 200 && response.data && response.data.create_tweet) {
                        console.log('✅ Tweet posted successfully!');
                        console.log('Tweet ID:', response.data.create_tweet.tweet_results?.result?.rest_id);
                        resolve(true);
                    } else {
                        console.error('❌ Failed to post tweet');
                        console.error('Status:', res.statusCode);
                        console.error('Response:', JSON.stringify(response, null, 2));
                        resolve(false);
                    }
                } catch (e) {
                    console.error('❌ Error parsing response:', e);
                    console.error('Raw response:', data);
                    resolve(false);
                }
            });
        });
        
        req.on('error', (e) => {
            console.error('❌ Request error:', e);
            reject(e);
        });
        
        req.write(postData);
        req.end();
    });
}

// Post from queue
async function postFromQueue() {
    try {
        const queuePath = '/root/Projects/dear-bubbe-admin/logs/twitter-queue.json';
        const queue = JSON.parse(await fs.readFile(queuePath, 'utf8'));
        
        if (queue.length === 0) {
            console.log('No tweets in queue');
            return false;
        }
        
        // Get first unposted tweet
        const toPost = queue.find(t => t.status === 'queued');
        if (!toPost) {
            console.log('All tweets already posted');
            return false;
        }
        
        console.log(`\n📤 Posting queued tweet from ${toPost.timestamp}`);
        
        // Post it
        const success = await postTweet(toPost.text);
        
        if (success) {
            // Update queue
            toPost.status = 'posted';
            toPost.postedAt = new Date().toISOString();
            await fs.writeFile(queuePath, JSON.stringify(queue, null, 2));
            console.log('✅ Queue updated');
        }
        
        return success;
        
    } catch (error) {
        console.error('Error posting from queue:', error);
        return false;
    }
}

// Setup function to save tokens
async function setup(auth_token, ct0) {
    const tokens = {
        auth_token,
        ct0,
        bearer: X_CONFIG.bearer
    };
    await saveTokens(tokens);
    console.log('✅ X.com tokens configured successfully!');
}

module.exports = {
    postTweet,
    postFromQueue,
    setup
};

// If run directly
if (require.main === module) {
    const args = process.argv.slice(2);
    
    if (args[0] === 'setup' && args[1] && args[2]) {
        // Setup mode: node x-graphql-poster.js setup AUTH_TOKEN CT0_TOKEN
        setup(args[1], args[2])
            .then(() => console.log('Setup complete!'))
            .catch(console.error);
    } else if (args[0] === 'test') {
        // Test mode: node x-graphql-poster.js test "Your test tweet"
        const testText = args[1] || "Test from Bubbe AI at " + new Date().toISOString();
        postTweet(testText)
            .then(result => {
                console.log('Test result:', result ? 'SUCCESS' : 'FAILED');
                process.exit(result ? 0 : 1);
            })
            .catch(console.error);
    } else {
        // Default: post from queue
        postFromQueue()
            .then(result => {
                console.log('Result:', result ? 'SUCCESS' : 'FAILED');
                process.exit(result ? 0 : 1);
            })
            .catch(console.error);
    }
}