← back to Dear Bubbe Nextjs

lib/instagram-poster.js

473 lines

#!/usr/bin/env node

const { chromium } = require('playwright');
const fs = require('fs').promises;
const path = require('path');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const { generateInstagramImage, generateCaption } = require('./instagram-image-generator');
const { anonymizeMessage } = require('./anonymous-user-poster');

// Configuration
const CONFIG = {
    INSTAGRAM_USERNAME: 'dearBubbe',
    INSTAGRAM_PASSWORD: '*Instaaccess911*',
    COOKIES_PATH: '/root/Projects/dear-bubbe-nextjs/lib/instagram-cookies.json',
    STATE_FILE: '/root/Projects/dear-bubbe-admin/logs/instagram-state.json',
    POSTS_PER_DAY: 3,
    MIN_INTERVAL_HOURS: 6
};

// Get user messages
async function getUserMessages() {
    try {
        const { stdout } = await execPromise(
            `pm2 logs bubbe-ai --lines 1000 --nostream | grep -E "message:" | grep -v "__init__" | grep -v "__returning" | head -50`
        );
        
        const messages = [];
        const lines = stdout.split('\n').filter(line => line.trim());
        
        for (const line of lines) {
            const match = line.match(/message:\s*["'](.+?)["']/);
            if (match && match[1]) {
                const msg = match[1].trim();
                if (msg.length > 20 && !msg.includes('__') && !msg.startsWith('test') && !msg.includes('favicon')) {
                    messages.push(msg);
                }
            }
        }
        
        return [...new Set(messages)];
    } catch (error) {
        console.error('Error getting messages:', error);
        return [];
    }
}

// Load state
async function loadState() {
    try {
        const data = await fs.readFile(CONFIG.STATE_FILE, 'utf8');
        return JSON.parse(data);
    } catch {
        return {
            postedMessages: [],
            lastPostTime: null,
            totalPosts: 0,
            dailyPosts: 0,
            lastResetDate: new Date().toDateString()
        };
    }
}

// Save state
async function saveState(state) {
    await fs.writeFile(CONFIG.STATE_FILE, JSON.stringify(state, null, 2));
}

// Load cookies
async function loadCookies(context) {
    try {
        const cookies = JSON.parse(await fs.readFile(CONFIG.COOKIES_PATH, 'utf8'));
        await context.addCookies(cookies);
        return true;
    } catch {
        return false;
    }
}

// Save cookies
async function saveCookies(context) {
    const cookies = await context.cookies();
    await fs.writeFile(CONFIG.COOKIES_PATH, JSON.stringify(cookies, null, 2));
}

// Post to Instagram
async function postToInstagram(imagePath, caption) {
    const browser = await chromium.launch({
        headless: true, // Run headless on server
        args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage',
            '--disable-gpu',
            '--no-first-run',
            '--no-zygote',
            '--single-process'
        ]
    });
    
    try {
        const context = await browser.newContext({
            viewport: { width: 390, height: 844 }, // iPhone 14 Pro viewport
            userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
            isMobile: true,
            hasTouch: true,
            deviceScaleFactor: 3,
            locale: 'en-US'
        });
        
        const page = await context.newPage();
        
        // Enable console logging for debugging
        page.on('console', msg => {
            if (msg.type() === 'error') {
                console.log('Page error:', msg.text());
            }
        });
        
        // Load cookies
        const cookiesLoaded = await loadCookies(context);
        
        // Go to Instagram
        console.log('📱 Navigating to Instagram...');
        await page.goto('https://www.instagram.com/', { waitUntil: 'domcontentloaded', timeout: 60000 });
        await page.waitForTimeout(3000);
        
        // Check if we need to login
        const loginFormPresent = await page.locator('input[name="username"]').isVisible().catch(() => false);
        const loginButtonPresent = await page.locator('button:has-text("Log in"), button:has-text("Log In")').isVisible().catch(() => false);
        
        if ((loginFormPresent || loginButtonPresent) && !cookiesLoaded) {
            console.log('📱 Logging in to Instagram...');
            
            // If we see the initial login button, click it first
            if (loginButtonPresent && !loginFormPresent) {
                await page.click('button:has-text("Log in"), button:has-text("Log In")');
                await page.waitForTimeout(2000);
            }
            
            // Enter credentials
            await page.fill('input[name="username"]', CONFIG.INSTAGRAM_USERNAME);
            await page.waitForTimeout(1000);
            
            await page.fill('input[name="password"]', CONFIG.INSTAGRAM_PASSWORD);
            await page.waitForTimeout(1000);
            
            // Submit login form - try multiple selectors
            const loginButton = await page.locator('button[type="submit"], button:has-text("Log in"), button:has-text("Log In"), div[role="button"]:has-text("Log in")').first();
            await loginButton.click();
            
            console.log('⏳ Waiting for login to complete...');
            await page.waitForTimeout(8000);
            
            // Handle potential popups after login
            const notNowButtons = ['text="Not Now"', 'text="Not now"', 'button:has-text("Not Now")', 'button:has-text("Not now")'];
            for (const selector of notNowButtons) {
                await page.click(selector).catch(() => {});
                await page.waitForTimeout(1000);
            }
            
            // Save cookies after successful login
            await saveCookies(context);
            console.log('✅ Login successful, cookies saved');
        }
        
        // Wait for page to fully load
        await page.waitForTimeout(3000);
        
        // Instagram mobile web has different UI - we need to navigate to create page directly
        console.log('📸 Navigating to create post page...');
        
        // Method 1: Try direct navigation to create page
        await page.goto('https://www.instagram.com/create/select/', { waitUntil: 'domcontentloaded' }).catch(async () => {
            // Method 2: Try clicking the plus icon or create button
            console.log('Trying alternative navigation methods...');
            
            // Look for various possible selectors for the new post button
            const newPostSelectors = [
                'svg[aria-label="New post"]',
                'svg[aria-label="New Post"]',
                'a[href="/create/select/"]',
                'a[href*="create"]',
                '[aria-label="Create"]',
                'svg[aria-label="Create"]',
                // Plus icon variants
                'svg path[d*="M12.003"]', // Common plus icon path
                'div[role="button"]:has(svg)',
                // Bottom navigation bar icons
                'nav a[href*="create"]',
                'nav svg[aria-label*="Create"]',
                'nav svg[aria-label*="New"]'
            ];
            
            let clicked = false;
            for (const selector of newPostSelectors) {
                try {
                    const element = await page.locator(selector).first();
                    if (await element.isVisible({ timeout: 1000 })) {
                        await element.click();
                        clicked = true;
                        console.log(`✅ Clicked create button using: ${selector}`);
                        break;
                    }
                } catch (e) {
                    // Continue trying other selectors
                }
            }
            
            if (!clicked) {
                // Last resort: Try to find the file input directly
                console.log('Could not find create button, looking for file input...');
            }
        });
        
        await page.waitForTimeout(3000);
        
        // Handle file upload
        console.log('📤 Uploading image...');
        
        // Look for file input - Instagram often hides it
        let fileInputFound = false;
        const fileInputSelectors = [
            'input[type="file"][accept*="image"]',
            'input[type="file"]',
            'input[accept="image/jpeg,image/png,image/heic,image/heif,video/mp4,video/quicktime"]'
        ];
        
        for (const selector of fileInputSelectors) {
            const fileInput = await page.locator(selector).first();
            if (await fileInput.count() > 0) {
                await fileInput.setInputFiles(imagePath);
                fileInputFound = true;
                console.log('✅ Image selected');
                break;
            }
        }
        
        if (!fileInputFound) {
            // If no file input found, try clicking "Select from computer" or similar button first
            const selectButtons = [
                'button:has-text("Select from computer")',
                'button:has-text("Select From Computer")',
                'text="Select from computer"',
                'button:has-text("Choose file")',
                'button:has-text("Upload")'
            ];
            
            for (const selector of selectButtons) {
                await page.click(selector).catch(() => {});
            }
            await page.waitForTimeout(2000);
            
            // Try file input again
            const fileInput = await page.locator('input[type="file"]').first();
            await fileInput.setInputFiles(imagePath);
        }
        
        await page.waitForTimeout(5000);
        
        // Click through the flow - Next buttons
        console.log('📝 Processing image...');
        
        // Look for Next/OK buttons
        const nextSelectors = [
            'button:has-text("Next")',
            'button:has-text("OK")',
            'div[role="button"]:has-text("Next")',
            'button[type="button"]:has-text("Next")'
        ];
        
        // First Next (after image selection)
        for (const selector of nextSelectors) {
            const nextBtn = await page.locator(selector).first();
            if (await nextBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
                await nextBtn.click();
                console.log('Clicked Next (crop/adjust)');
                break;
            }
        }
        await page.waitForTimeout(3000);
        
        // Second Next (after filters/editing)
        for (const selector of nextSelectors) {
            const nextBtn = await page.locator(selector).first();
            if (await nextBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
                await nextBtn.click();
                console.log('Clicked Next (filters)');
                break;
            }
        }
        await page.waitForTimeout(3000);
        
        // Add caption
        console.log('✍️ Adding caption...');
        const captionSelectors = [
            'textarea[aria-label*="caption" i]',
            'textarea[aria-label*="write" i]',
            'textarea[placeholder*="caption" i]',
            'textarea[placeholder*="Write" i]',
            'textarea:not([aria-label*="Title"])',
            'div[contenteditable="true"][role="textbox"]'
        ];
        
        let captionAdded = false;
        for (const selector of captionSelectors) {
            try {
                const captionField = await page.locator(selector).first();
                if (await captionField.isVisible({ timeout: 1000 })) {
                    await captionField.click();
                    await captionField.fill(caption);
                    captionAdded = true;
                    console.log('✅ Caption added');
                    break;
                }
            } catch (e) {
                // Continue trying
            }
        }
        
        if (!captionAdded) {
            console.log('⚠️ Could not add caption, continuing...');
        }
        
        await page.waitForTimeout(2000);
        
        // Share/Post the image
        console.log('🚀 Sharing post...');
        const shareSelectors = [
            'button:has-text("Share")',
            'button:has-text("Post")',
            'div[role="button"]:has-text("Share")',
            'button[type="button"]:has-text("Share")'
        ];
        
        for (const selector of shareSelectors) {
            const shareBtn = await page.locator(selector).first();
            if (await shareBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
                await shareBtn.click();
                console.log('✅ Clicked Share button');
                break;
            }
        }
        
        // Wait for post to complete
        await page.waitForTimeout(8000);
        
        // Save cookies for next time
        await saveCookies(context);
        
        console.log('✅ Posted to Instagram successfully!');
        return { success: true };
        
    } catch (error) {
        console.error('❌ Error posting to Instagram:', error.message);
        
        // Take a screenshot for debugging
        try {
            const screenshotPath = `/root/Projects/dear-bubbe-admin/logs/instagram-error-${Date.now()}.png`;
            await page.screenshot({ path: screenshotPath, fullPage: true });
            console.log(`📸 Error screenshot saved: ${screenshotPath}`);
        } catch (e) {
            // Ignore screenshot errors
        }
        
        return { success: false, error: error.message };
    } finally {
        await browser.close();
    }
}

// Main posting function
async function postAnonymousToInstagram() {
    console.log('📸 Instagram Anonymous Poster');
    console.log('================================');
    
    // Load state
    const state = await loadState();
    
    // Reset daily counter
    const today = new Date().toDateString();
    if (today !== state.lastResetDate) {
        state.dailyPosts = 0;
        state.lastResetDate = today;
    }
    
    // Check limits
    if (state.dailyPosts >= CONFIG.POSTS_PER_DAY) {
        console.log(`📊 Daily limit reached (${state.dailyPosts}/${CONFIG.POSTS_PER_DAY})`);
        return;
    }
    
    // Check interval
    if (state.lastPostTime) {
        const hoursSince = (Date.now() - new Date(state.lastPostTime).getTime()) / (1000 * 60 * 60);
        if (hoursSince < CONFIG.MIN_INTERVAL_HOURS) {
            console.log(`⏱️ Wait ${(CONFIG.MIN_INTERVAL_HOURS - hoursSince).toFixed(1)} more hours`);
            return;
        }
    }
    
    // Get messages
    const messages = await getUserMessages();
    const unposted = messages.filter(msg => !state.postedMessages.includes(msg));
    
    if (unposted.length === 0) {
        console.log('❌ No new messages');
        return;
    }
    
    // Select message
    const selected = unposted[Math.floor(Math.random() * unposted.length)];
    console.log(`📝 Selected: "${selected.substring(0, 50)}..."`);
    
    // Generate image
    const timestamp = Date.now();
    const imagePath = `/root/Projects/dear-bubbe-admin/public/instagram-${timestamp}.png`;
    await generateInstagramImage(selected, imagePath);
    console.log(`🎨 Image created: ${imagePath}`);
    
    // Generate caption
    const caption = generateCaption(selected);
    console.log(`📱 Caption: ${caption.substring(0, 100)}...`);
    
    // Post to Instagram
    const result = await postToInstagram(imagePath, caption);
    
    if (result.success) {
        // Update state
        state.postedMessages.push(selected);
        state.lastPostTime = new Date().toISOString();
        state.totalPosts++;
        state.dailyPosts++;
        
        if (state.postedMessages.length > 100) {
            state.postedMessages = state.postedMessages.slice(-100);
        }
        
        await saveState(state);
        console.log(`📊 Posted ${state.dailyPosts}/${CONFIG.POSTS_PER_DAY} today`);
        console.log(`📈 Total Instagram posts: ${state.totalPosts}`);
        console.log(`🖼️ Image: http://45.61.58.125:7902/instagram-${timestamp}.png`);
    }
}

// Continuous posting
async function continuousPost() {
    console.log('🚀 Starting Instagram Continuous Poster');
    console.log(`📸 ${CONFIG.POSTS_PER_DAY} posts per day`);
    console.log(`⏱️ Every ${CONFIG.MIN_INTERVAL_HOURS} hours`);
    console.log('=' .repeat(40));
    
    // Post immediately
    await postAnonymousToInstagram();
    
    // Check every 2 hours
    setInterval(async () => {
        await postAnonymousToInstagram();
    }, 2 * 60 * 60 * 1000);
}

// Run
if (require.main === module) {
    const args = process.argv.slice(2);
    if (args[0] === 'continuous') {
        continuousPost().catch(console.error);
    } else {
        postAnonymousToInstagram().catch(console.error);
    }
}

module.exports = { postAnonymousToInstagram };