← back to Dear Bubbe Nextjs

archived/social-posters/bluesky-autoposter.js

278 lines

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

// Configuration
const BLUESKY_USERNAME = 'Dear Bubbe';
const BLUESKY_PASSWORD = '*Blueaccess911*';
const BLUESKY_HANDLE = 'dearbubbe.bsky.social';
const COOKIES_PATH = path.join(__dirname, 'bluesky-cookies.json');
const POSTED_IDS_PATH = path.join(__dirname, 'posted-bluesky.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",
    "Bubbe's savage advice",
    "Bubbe pulls no punches"
];

// Name anonymization function (same as Twitter)
function anonymizeText(text) {
    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',
        
        // 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',
        
        // Spouse/partner references
        'my husband': 'my partner', 'my wife': 'my partner', 
        'my boyfriend': 'my partner', 'my girlfriend': 'my partner'
    };
    
    let anonymized = text;
    
    for (const [original, replacement] of Object.entries(nameReplacements)) {
        const regex = new RegExp(`\\b${original}\\b`, 'gi');
        anonymized = anonymized.replace(regex, replacement);
    }
    
    // Remove sensitive info
    anonymized = anonymized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]');
    anonymized = anonymized.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[phone]');
    anonymized = anonymized.replace(/\d+\s+[A-Za-z]+\s+(Street|St|Avenue|Ave|Road|Rd)/gi, '[address]');
    
    return anonymized;
}

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

// Format post content (Bluesky has 300 char limit)
function formatPost(userMessage, bubbeResponse) {
    const intro = getRandomIntro();
    const anonymizedUser = anonymizeText(userMessage).substring(0, 80);
    const anonymizedResponse = anonymizeText(bubbeResponse).substring(0, 120);
    
    const post = `${intro}:\n\nQ: "${anonymizedUser}..."\n\nBubbe: "${anonymizedResponse}..."\n\n#BubbeAI #Bubbe #JewishGrandma\n\nVisit Bubbe.AI`;
    
    // Ensure post is under 300 characters
    if (post.length > 300) {
        return `${intro}: "${anonymizedResponse.substring(0, 100)}..." #BubbeAI\n\nVisit Bubbe.AI`.substring(0, 300);
    }
    
    return post;
}

// 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);
            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 postToBluesky(userMessage, bubbeResponse, messageId = null) {
    // Skip if no real content
    if (!userMessage || !bubbeResponse || 
        userMessage.length < 10 || bubbeResponse.length < 10) {
        console.log('Skipping Bluesky - insufficient content');
        return false;
    }
    
    // Check if already posted
    if (messageId && await isAlreadyPosted(messageId)) {
        console.log('Already posted to Bluesky:', 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 Bluesky
        await page.goto('https://bsky.app', { waitUntil: 'networkidle2' });
        
        // Check if we need to login
        const needsLogin = await page.evaluate(() => {
            return window.location.pathname === '/';
        });
        
        if (needsLogin && !cookiesLoaded) {
            console.log('Logging in to Bluesky...');
            
            // Click sign in button
            await page.waitForSelector('button:has-text("Sign in")', { timeout: 10000 });
            await page.click('button:has-text("Sign in")');
            
            // Enter username
            await page.waitForSelector('input[placeholder*="Username"]', { timeout: 10000 });
            await page.type('input[placeholder*="Username"]', BLUESKY_HANDLE);
            
            // Enter password
            await page.type('input[type="password"]', BLUESKY_PASSWORD);
            
            // Click login
            await page.click('button[type="submit"]');
            
            // Wait for login to complete
            await page.waitForNavigation({ waitUntil: 'networkidle2' });
            
            // Save cookies for next time
            await saveCookies(page);
            console.log('Bluesky login successful, cookies saved');
        }
        
        // Format the post
        const postContent = formatPost(userMessage, bubbeResponse);
        console.log('Posting to Bluesky:', postContent.substring(0, 100) + '...');
        
        // Click compose button
        await page.waitForSelector('[aria-label="Compose new post"]', { timeout: 10000 });
        await page.click('[aria-label="Compose new post"]');
        
        // Wait for compose modal
        await page.waitForSelector('textarea[placeholder*="What\'s up"]', { timeout: 10000 });
        
        // Type the post
        await page.type('textarea[placeholder*="What\'s up"]', postContent);
        
        // Wait a moment
        await page.waitForTimeout(1000);
        
        // Click post button
        await page.click('button:has-text("Post")');
        
        // Wait for post to complete
        await page.waitForTimeout(3000);
        
        // Mark as posted
        if (messageId) {
            await markAsPosted(messageId);
        }
        
        console.log('Bluesky post successful!');
        return true;
        
    } catch (error) {
        console.error('Error posting to Bluesky:', error);
        return false;
    } finally {
        await browser.close();
    }
}

// Auto-post from chat logs (shared with Twitter)
async function autoPostFromChats() {
    try {
        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
        const interestingChats = recentChats
            .filter(chat => chat.userMessage && chat.bubbeResponse)
            .sort((a, b) => b.bubbeResponse.length - a.bubbeResponse.length)
            .slice(0, 5);
        
        // Post one random interesting chat
        if (interestingChats.length > 0) {
            const chat = interestingChats[Math.floor(Math.random() * interestingChats.length)];
            await postToBluesky(chat.userMessage, chat.bubbeResponse, 'bsky-' + chat.id);
        }
        
    } catch (error) {
        console.error('Error in Bluesky auto-post:', error);
    }
}

// Export functions
module.exports = {
    postToBluesky,
    autoPostFromChats,
    anonymizeText,
    formatPost
};

// If run directly, test it
if (require.main === module) {
    const testUser = "My wife Karen is always complaining about money!";
    const testBubbe = "Money? She should complain! You probably spend it all on nonsense. Get a better job or stop buying schmatte!";
    
    postToBluesky(testUser, testBubbe, 'bsky-test-' + Date.now())
        .then(result => {
            console.log('Bluesky test post result:', result);
            process.exit(result ? 0 : 1);
        });
}