← back to Dear Bubbe Nextjs

archived/social-posters/twitter-autoposter.js

290 lines

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

// Configuration
const TWITTER_USERNAME = 'DearBubbe';
const TWITTER_PASSWORD = '*Twitteraccess911*';
const COOKIES_PATH = path.join(__dirname, 'twitter-cookies.json');
const POSTED_IDS_PATH = path.join(__dirname, 'posted-tweets.json');

// Bubbe intro phrases for variety
const BUBBE_INTROS = [
    "Bubbe tells it like it is",
    "Bubbe's got no filter",
    "Bubbe says what you're thinking",
    "Bubbe drops truth bombs",
    "Bubbe keeps it real",
    "Bubbe's wisdom hits different",
    "Bubbe doesn't sugarcoat",
    "Bubbe serves reality checks",
    "Bubbe's brutal honesty",
    "Bubbe speaks facts"
];

// Name anonymization function
function anonymizeText(text) {
    // Common names to replace with generic alternatives
    const nameReplacements = {
        // Male names
        'john': 'David', 'mike': 'Michael', 'steve': 'Robert', 'james': 'William',
        'david': 'John', 'robert': 'James', 'michael': 'Thomas', 'william': 'Charles',
        'richard': 'Joseph', 'joseph': 'Daniel', 'thomas': 'Paul', 'charles': 'Mark',
        'daniel': 'George', 'paul': 'Steven', 'mark': 'Edward', 'george': 'Brian',
        
        // Female names  
        'mary': 'Sarah', 'jennifer': 'Jessica', 'linda': 'Emma', 'patricia': 'Emily',
        'elizabeth': 'Anna', 'susan': 'Rachel', 'jessica': 'Rebecca', 'sarah': 'Hannah',
        'karen': 'Michelle', 'nancy': 'Laura', 'betty': 'Amanda', 'dorothy': 'Nicole',
        'lisa': 'Samantha', 'michelle': 'Ashley', 'amanda': 'Sophia', 'emily': 'Grace',
        
        // Spouse/partner references
        'my husband': 'my partner', 'my wife': 'my partner', 
        'my boyfriend': 'my partner', 'my girlfriend': 'my partner',
        'my fiance': 'my partner', 'my fiancee': 'my partner'
    };
    
    let anonymized = text;
    
    // Replace specific names (case-insensitive)
    for (const [original, replacement] of Object.entries(nameReplacements)) {
        const regex = new RegExp(`\\b${original}\\b`, 'gi');
        anonymized = anonymized.replace(regex, replacement);
    }
    
    // Remove email addresses
    anonymized = anonymized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]');
    
    // Remove phone numbers
    anonymized = anonymized.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[phone]');
    
    // Remove addresses (basic pattern)
    anonymized = anonymized.replace(/\d+\s+[A-Za-z]+\s+(Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Lane|Ln|Drive|Dr)/gi, '[address]');
    
    // Remove company names that might be identifying
    anonymized = anonymized.replace(/@[A-Za-z0-9_]+/g, '@[company]');
    
    return anonymized;
}

// Get random intro
function getRandomIntro() {
    return BUBBE_INTROS[Math.floor(Math.random() * BUBBE_INTROS.length)];
}

// Format tweet content
function formatTweet(userMessage, bubbeResponse) {
    const intro = getRandomIntro();
    const anonymizedUser = anonymizeText(userMessage).substring(0, 100);
    const anonymizedResponse = anonymizeText(bubbeResponse).substring(0, 150);
    
    const tweet = `${intro}:\n\nUser: "${anonymizedUser}..."\n\nBubbe: "${anonymizedResponse}..."\n\n#BubbeAI #Bubbe #NoFilter #JewishGrandma\n\nVisit Bubbe.AI`;
    
    // Ensure tweet is under 280 characters
    if (tweet.length > 280) {
        const shortened = `${intro}: "${anonymizedResponse.substring(0, 120)}..." #BubbeAI #Bubbe\n\nVisit Bubbe.AI`;
        return shortened.substring(0, 280);
    }
    
    return tweet;
}

// Save cookies for future sessions
async function saveCookies(page) {
    const cookies = await page.cookies();
    await fs.writeFile(COOKIES_PATH, JSON.stringify(cookies, null, 2));
}

// Load cookies from previous session
async function loadCookies(page) {
    try {
        const cookiesString = await fs.readFile(COOKIES_PATH, 'utf8');
        const cookies = JSON.parse(cookiesString);
        await page.setCookie(...cookies);
        return true;
    } catch (error) {
        return false;
    }
}

// Check if already posted
async function isAlreadyPosted(messageId) {
    try {
        const postedIds = JSON.parse(await fs.readFile(POSTED_IDS_PATH, 'utf8'));
        return postedIds.includes(messageId);
    } catch (error) {
        return false;
    }
}

// Mark as posted
async function markAsPosted(messageId) {
    try {
        let postedIds = [];
        try {
            postedIds = JSON.parse(await fs.readFile(POSTED_IDS_PATH, 'utf8'));
        } catch (error) {
            // File doesn't exist yet
        }
        
        if (!postedIds.includes(messageId)) {
            postedIds.push(messageId);
            // Keep only last 1000 IDs to prevent file from growing too large
            if (postedIds.length > 1000) {
                postedIds = postedIds.slice(-1000);
            }
            await fs.writeFile(POSTED_IDS_PATH, JSON.stringify(postedIds, null, 2));
        }
    } catch (error) {
        console.error('Error marking as posted:', error);
    }
}

// Main posting function
async function postToTwitter(userMessage, bubbeResponse, messageId = null) {
    // Skip if no real content
    if (!userMessage || !bubbeResponse || 
        userMessage.length < 10 || bubbeResponse.length < 10) {
        console.log('Skipping - insufficient content');
        return false;
    }
    
    // Check if already posted
    if (messageId && await isAlreadyPosted(messageId)) {
        console.log('Already posted:', messageId);
        return false;
    }
    
    const browser = await puppeteer.launch({
        headless: 'new',
        args: ['--no-sandbox', '--disable-setuid-sandbox']
    });
    
    try {
        const page = await browser.newPage();
        await page.setViewport({ width: 1280, height: 720 });
        
        // Try to load cookies
        const cookiesLoaded = await loadCookies(page);
        
        // Navigate to Twitter
        await page.goto('https://x.com', { waitUntil: 'networkidle2' });
        
        // Check if we need to login
        const needsLogin = await page.evaluate(() => {
            return !document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]');
        });
        
        if (needsLogin && !cookiesLoaded) {
            console.log('Logging in to Twitter...');
            
            // Click login button
            await page.click('[href="/login"]');
            await page.waitForSelector('input[autocomplete="username"]', { timeout: 10000 });
            
            // Enter username
            await page.type('input[autocomplete="username"]', TWITTER_USERNAME);
            await page.keyboard.press('Enter');
            
            // Wait for password field
            await page.waitForSelector('input[type="password"]', { timeout: 10000 });
            await page.type('input[type="password"]', TWITTER_PASSWORD);
            await page.keyboard.press('Enter');
            
            // Wait for login to complete
            await page.waitForSelector('[data-testid="SideNav_AccountSwitcher_Button"]', { timeout: 20000 });
            
            // Save cookies for next time
            await saveCookies(page);
            console.log('Login successful, cookies saved');
        }
        
        // Format the tweet
        const tweetContent = formatTweet(userMessage, bubbeResponse);
        console.log('Posting tweet:', tweetContent.substring(0, 100) + '...');
        
        // Click compose tweet button
        await page.waitForSelector('[data-testid="SideNav_NewTweet_Button"]', { timeout: 10000 });
        await page.click('[data-testid="SideNav_NewTweet_Button"]');
        
        // Wait for compose modal
        await page.waitForSelector('[data-testid="tweetTextarea_0"]', { timeout: 10000 });
        
        // Type the tweet
        await page.type('[data-testid="tweetTextarea_0"]', tweetContent);
        
        // Wait a moment for character count to update
        await page.waitForTimeout(1000);
        
        // Click tweet button
        await page.click('[data-testid="tweetButtonInline"]');
        
        // Wait for tweet to be posted
        await page.waitForTimeout(3000);
        
        // Mark as posted
        if (messageId) {
            await markAsPosted(messageId);
        }
        
        console.log('Tweet posted successfully!');
        return true;
        
    } catch (error) {
        console.error('Error posting to Twitter:', error);
        return false;
    } finally {
        await browser.close();
    }
}

// Auto-post from chat logs
async function autoPostFromChats() {
    try {
        // Read recent chats from the chat log file
        const chatLogPath = '/root/Projects/dear-bubbe-nextjs/logs/chat-history.json';
        const chats = JSON.parse(await fs.readFile(chatLogPath, 'utf8'));
        
        // Get chats from last 24 hours
        const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
        const recentChats = chats.filter(chat => 
            chat.timestamp && new Date(chat.timestamp).getTime() > oneDayAgo
        );
        
        // Sort by engagement (length of response)
        const interestingChats = recentChats
            .filter(chat => chat.userMessage && chat.bubbeResponse)
            .sort((a, b) => b.bubbeResponse.length - a.bubbeResponse.length)
            .slice(0, 5); // Top 5 most interesting
        
        // Post one random interesting chat
        if (interestingChats.length > 0) {
            const chat = interestingChats[Math.floor(Math.random() * interestingChats.length)];
            await postToTwitter(chat.userMessage, chat.bubbeResponse, chat.id);
        }
        
    } catch (error) {
        console.error('Error in auto-post:', error);
    }
}

// Export functions
module.exports = {
    postToTwitter,
    autoPostFromChats,
    anonymizeText,
    formatTweet
};

// If run directly, post a test tweet
if (require.main === module) {
    // Test with sample data
    const testUser = "My husband Steve never helps with the dishes and I'm tired of it!";
    const testBubbe = "Steve? More like 'Leave'! You're washing his schmatte underwear but he can't wash a plate? Tell that putz either he helps or you stop cooking!";
    
    postToTwitter(testUser, testBubbe, 'test-' + Date.now())
        .then(result => {
            console.log('Test post result:', result);
            process.exit(result ? 0 : 1);
        });
}