← back to Dear Bubbe Nextjs

archived/social-posters/twitter-anonymous-poster.js

292 lines

#!/usr/bin/env node

const fs = require('fs').promises;
const path = require('path');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const playwright = require('playwright');

// Import anonymization from Bluesky poster
const { anonymizeMessage } = require('./anonymous-user-poster');

// Configuration
const CONFIG = {
  TWITTER_USERNAME: 'DearBubbe',
  POSTS_PER_DAY: 20,
  MIN_INTERVAL_MINUTES: 30,
  STATE_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-anonymous-state.json',
  CREDENTIALS_FILE: '/root/Projects/dear-bubbe-nextjs/lib/twitter-cookies.json'
};

// Get recent user messages from logs
async function getUserMessages() {
  try {
    const { stdout } = await execPromise(
      `pm2 logs bubbe-ai --lines 500 --nostream | grep -E "message:" | grep -v "__init__" | grep -v "__returning" | head -30`
    );
    
    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') &&
            !msg.includes('http')) {
          messages.push(msg);
        }
      }
    }
    
    return [...new Set(messages)];
  } catch (error) {
    console.error('Error getting user messages:', error);
    return [];
  }
}

// Format post for Twitter
function formatPostForTwitter(userMessage) {
  const intros = [
    "Someone just asked Bubbe:",
    "Real question today:",
    "Anonymous asks:",
    "User dilemma:",
    "Today's confession:",
    "Someone needs advice:"
  ];
  
  const intro = intros[Math.floor(Math.random() * intros.length)];
  const anonymized = anonymizeMessage(userMessage);
  
  // Twitter has 280 char limit
  const maxLength = 200; // Leave room for intro, hashtags, and link
  const truncated = anonymized.length > maxLength ? 
    anonymized.substring(0, maxLength) + '...' : 
    anonymized;
  
  const hashtags = '#BubbeAI #JewishGrandma #Advice';
  return `${intro}\n"${truncated}"\n\n${hashtags}\nbubbe.ai`;
}

// 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));
}

// Post to Twitter using Playwright
async function postToTwitter(text) {
  const browser = await playwright.chromium.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });

  try {
    const context = await browser.newContext({
      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
      viewport: { width: 1280, height: 720 }
    });

    // Load cookies if available
    try {
      const cookies = JSON.parse(await fs.readFile(CONFIG.CREDENTIALS_FILE, 'utf8'));
      if (cookies && cookies.length > 0) {
        await context.addCookies(cookies);
      }
    } catch (e) {
      console.log('⚠️ No saved cookies, will need to login');
    }

    const page = await context.newPage();
    
    // Go to Twitter
    await page.goto('https://x.com/compose/post', { waitUntil: 'networkidle', timeout: 30000 });
    
    // Wait for compose box
    await page.waitForTimeout(3000);
    
    // Check if we need to login
    const needsLogin = await page.$('input[autocomplete="username"]');
    if (needsLogin) {
      console.log('❌ Twitter login required - manual intervention needed');
      return { success: false, error: 'Login required' };
    }
    
    // Find the compose box and type
    const composeBox = await page.$('[data-testid="tweetTextarea_0"]');
    if (!composeBox) {
      console.log('❌ Could not find compose box');
      return { success: false, error: 'Compose box not found' };
    }
    
    // Type the message
    await composeBox.type(text, { delay: 50 });
    await page.waitForTimeout(1000);
    
    // Click Post button
    const postButton = await page.$('[data-testid="tweetButtonInline"]');
    if (!postButton) {
      console.log('❌ Could not find post button');
      return { success: false, error: 'Post button not found' };
    }
    
    await postButton.click();
    await page.waitForTimeout(3000);
    
    // Save cookies for next time
    const cookies = await context.cookies();
    await fs.writeFile(CONFIG.CREDENTIALS_FILE, JSON.stringify(cookies, null, 2));
    
    console.log('✅ Posted to Twitter successfully');
    return { success: true };
    
  } catch (error) {
    console.error('❌ Error posting to Twitter:', error);
    return { success: false, error: error.message };
  } finally {
    await browser.close();
  }
}

// Main posting function
async function postAnonymousToTwitter() {
  console.log('🐦 Twitter Anonymous User Content Poster');
  console.log('========================================');
  
  // Load state
  const state = await loadState();
  
  // Reset daily counter if needed
  const today = new Date().toDateString();
  if (today !== state.lastResetDate) {
    state.dailyPosts = 0;
    state.lastResetDate = today;
    console.log('📅 New day - reset counter');
  }
  
  // Check daily limit
  if (state.dailyPosts >= CONFIG.POSTS_PER_DAY) {
    console.log(`📊 Daily limit reached (${state.dailyPosts}/${CONFIG.POSTS_PER_DAY})`);
    return;
  }
  
  // Check minimum interval
  if (state.lastPostTime) {
    const timeSince = Date.now() - new Date(state.lastPostTime).getTime();
    const minInterval = CONFIG.MIN_INTERVAL_MINUTES * 60 * 1000;
    if (timeSince < minInterval) {
      const waitTime = Math.round((minInterval - timeSince) / 60000);
      console.log(`⏱️ Waiting ${waitTime} more minutes before next post`);
      return;
    }
  }
  
  // Get user messages
  const messages = await getUserMessages();
  console.log(`📝 Found ${messages.length} user messages`);
  
  // Filter out already posted
  const unposted = messages.filter(msg => !state.postedMessages.includes(msg));
  console.log(`📌 ${unposted.length} new messages to post`);
  
  if (unposted.length === 0) {
    console.log('❌ No new messages to post');
    return;
  }
  
  // Pick a random message
  const selectedMessage = unposted[Math.floor(Math.random() * unposted.length)];
  console.log(`\n🎯 Selected: "${selectedMessage.substring(0, 50)}..."`);
  
  // Format for Twitter
  const post = formatPostForTwitter(selectedMessage);
  console.log(`\n🐦 Tweet:\n${post}\n`);
  
  // Post to Twitter
  const result = await postToTwitter(post);
  
  if (result.success) {
    // Update state
    state.postedMessages.push(selectedMessage);
    state.lastPostTime = new Date().toISOString();
    state.totalPosts++;
    state.dailyPosts++;
    
    // Keep only last 100 messages
    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 posts: ${state.totalPosts}`);
  } else {
    console.error('❌ Failed to post:', result.error);
    
    // If login required, provide fallback
    if (result.error === 'Login required') {
      console.log('\n📋 MANUAL POST:');
      console.log('1. Go to: https://x.com/compose/post');
      console.log('2. Login if needed');
      console.log('3. Copy and paste:');
      console.log('-'.repeat(40));
      console.log(post);
      console.log('-'.repeat(40));
    }
  }
}

// Continuous posting loop
async function continuousPost() {
  console.log('🚀 Starting Twitter Continuous Anonymous Poster');
  console.log(`⏱️ Posting every ${CONFIG.MIN_INTERVAL_MINUTES} minutes`);
  console.log(`📊 Max ${CONFIG.POSTS_PER_DAY} posts per day`);
  console.log('=' .repeat(50));

  // Post immediately
  await postAnonymousToTwitter();

  // Continue at intervals
  setInterval(async () => {
    await postAnonymousToTwitter();
  }, CONFIG.MIN_INTERVAL_MINUTES * 60 * 1000);
}

// Handle shutdown
process.on('SIGINT', () => {
  console.log('\n👋 Shutting down Twitter poster...');
  process.exit(0);
});

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

module.exports = { postAnonymousToTwitter };