← back to Dear Bubbe Nextjs

archived/social-posters/twitter-scheduler.js

445 lines

const fs = require('fs').promises;
const path = require('path');
const { postTweet } = require('./twitter-v2-official');
const { optimizeTweet } = require('./twitter-optimizer');
const axios = require('axios');

// Configuration
const CONFIG = {
  MAX_DAILY_POSTS: 100,
  MIN_INTERVAL_MINUTES: 3,
  QUEUE_CHECK_INTERVAL: 60000, // Check every minute
  STATS_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-stats.json',
  QUEUE_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-queue.json',
  POSTED_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-posted.json',
  WEEKLY_REPORT_FILE: '/root/Projects/dear-bubbe-admin/logs/weekly-twitter-report.json'
};

// Track posting state
let postingState = {
  lastPostTime: null,
  postsToday: 0,
  dayStartTime: null,
  isRunning: false
};

// Load or initialize stats
async function loadStats() {
  try {
    const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8'));
    
    // Check if it's a new day
    const today = new Date().toDateString();
    if (stats.currentDay !== today) {
      stats.postsToday = 0;
      stats.currentDay = today;
      stats.dayStartTime = new Date().toISOString();
    }
    
    return stats;
  } catch (e) {
    // Initialize new stats
    return {
      postsToday: 0,
      currentDay: new Date().toDateString(),
      dayStartTime: new Date().toISOString(),
      lastPostTime: null,
      totalPosts: 0,
      weeklyPosts: [],
      startDate: new Date().toISOString()
    };
  }
}

// Save stats
async function saveStats(stats) {
  await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
}

// Check if we can post now (rate limiting)
function canPostNow(stats) {
  // Check daily limit
  if (stats.postsToday >= CONFIG.MAX_DAILY_POSTS) {
    console.log(`📊 Daily limit reached: ${stats.postsToday}/${CONFIG.MAX_DAILY_POSTS}`);
    return false;
  }
  
  // Check minimum interval
  if (stats.lastPostTime) {
    const timeSinceLastPost = Date.now() - new Date(stats.lastPostTime).getTime();
    const minInterval = CONFIG.MIN_INTERVAL_MINUTES * 60 * 1000;
    
    if (timeSinceLastPost < minInterval) {
      const waitTime = Math.ceil((minInterval - timeSinceLastPost) / 1000);
      console.log(`⏱️ Rate limit: Wait ${waitTime}s before next post`);
      return false;
    }
  }
  
  return true;
}

// Calculate optimal posting schedule
function calculatePostingSchedule(postsToday) {
  const now = new Date();
  const hour = now.getHours();
  const remainingToday = CONFIG.MAX_DAILY_POSTS - postsToday;
  
  if (remainingToday <= 0) return null;
  
  // Calculate hours remaining in day
  const hoursLeft = 24 - hour;
  
  // Optimal posting times (ET) - peak engagement
  const peakHours = [7, 9, 12, 15, 17, 19, 21]; // 7am, 9am, 12pm, 3pm, 5pm, 7pm, 9pm
  
  // If we're in a peak hour, post more frequently
  if (peakHours.includes(hour)) {
    return 3; // Post every 3 minutes during peak
  }
  
  // Otherwise, spread remaining posts throughout the day
  if (hoursLeft > 0) {
    const postsPerHour = Math.ceil(remainingToday / hoursLeft);
    const minutesBetweenPosts = Math.max(CONFIG.MIN_INTERVAL_MINUTES, Math.floor(60 / postsPerHour));
    return minutesBetweenPosts;
  }
  
  return CONFIG.MIN_INTERVAL_MINUTES;
}

// Generate content when queue is empty
async function generateContent() {
  console.log('🎨 Generating fresh content...');
  
  // Topics that get engagement
  const topics = [
    { user: "My mother-in-law criticized my cooking again", category: "family" },
    { user: "Should I quit my job without another lined up?", category: "career" },
    { user: "My boyfriend only texts me once a day", category: "dating" },
    { user: "I spent $500 on shoes, was that too much?", category: "money" },
    { user: "My kids won't call me back", category: "family" },
    { user: "Is 35 too old to start dating again?", category: "dating" },
    { user: "My boss is a total micromanager", category: "work" },
    { user: "Should I get married or stay single?", category: "relationships" },
    { user: "I haven't been to temple in years", category: "jewish" },
    { user: "My neighbor's dog won't stop barking", category: "life" },
    { user: "I think my friend is jealous of my success", category: "friendship" },
    { user: "Should I move back in with my parents to save money?", category: "adulting" },
    { user: "I'm 28 and still single, is something wrong with me?", category: "dating" },
    { user: "My husband forgot our anniversary", category: "marriage" },
    { user: "I hate my job but the pay is good", category: "career" },
    { user: "My adult children still ask me for money", category: "parenting" },
    { user: "Should I go on a second date if there's no spark?", category: "dating" },
    { user: "I lied to my partner about something small", category: "relationships" },
    { user: "Is it wrong to not want kids?", category: "life" },
    { user: "My best friend is getting divorced", category: "friendship" }
  ];
  
  // Pick random topic
  const topic = topics[Math.floor(Math.random() * topics.length)];
  
  // Generate Bubbe response using Claude
  try {
    const response = await axios.post('http://45.61.58.125:3011/api/chat', {
      message: topic.user,
      mode: 'bubbe',
      userId: 'scheduler-' + Date.now(),
      location: 'New York, NY'
    });
    
    if (response.data && response.data.response) {
      return {
        userMessage: topic.user,
        bubbeResponse: response.data.response,
        category: topic.category,
        generated: true,
        timestamp: new Date().toISOString()
      };
    }
  } catch (error) {
    console.error('❌ Failed to generate content:', error.message);
  }
  
  // Fallback to pre-written responses
  const fallbacks = [
    {
      userMessage: "My mother thinks I'm wasting my life",
      bubbeResponse: "Oy vey, maybe she's right! What are you doing with yourself? No spouse, no kids, no real job? Listen bubbeleh, mothers know. Get your act together before it's too late!",
      category: "family"
    },
    {
      userMessage: "Should I text my ex?",
      bubbeResponse: "Are you meshuga? NO! What are you, a glutton for punishment? They're an ex for a reason! Delete their number and find someone who deserves you, schmuck!",
      category: "dating"
    },
    {
      userMessage: "I'm thinking about getting a tattoo",
      bubbeResponse: "A tattoo? Oy gevalt! You know that's permanent, right? What will you tell your grandchildren? 'Oh, this blob used to be a butterfly?' Think before you ink, bubbeleh!",
      category: "life"
    }
  ];
  
  return fallbacks[Math.floor(Math.random() * fallbacks.length)];
}

// Post from queue or generate new content
async function postNext() {
  const stats = await loadStats();
  
  if (!canPostNow(stats)) {
    return false;
  }
  
  try {
    // Check queue first
    let queue = [];
    try {
      queue = JSON.parse(await fs.readFile(CONFIG.QUEUE_FILE, 'utf8'));
    } catch (e) {
      // No queue file
    }
    
    // Filter unposted items
    const unposted = queue.filter(item => item.status !== 'posted');
    
    let content;
    if (unposted.length > 0) {
      // Use queued content
      content = unposted[0];
      content.status = 'posting';
    } else {
      // Generate new content
      content = await generateContent();
      if (!content) {
        console.log('⚠️ No content to post');
        return false;
      }
    }
    
    // Optimize and post
    const tweet = optimizeTweet(content.userMessage, content.bubbeResponse);
    console.log(`\n📤 Posting tweet #${stats.postsToday + 1}/${CONFIG.MAX_DAILY_POSTS}`);
    console.log(`Tweet: ${tweet.substring(0, 100)}...`);
    
    const result = await postTweet(tweet);
    
    if (result.success) {
      // Update stats
      stats.postsToday++;
      stats.totalPosts++;
      stats.lastPostTime = new Date().toISOString();
      
      // Add to weekly tracking
      if (!stats.weeklyPosts) stats.weeklyPosts = [];
      stats.weeklyPosts.push({
        date: new Date().toISOString(),
        tweetId: result.tweetId,
        url: `https://x.com/DearBubbe/status/${result.tweetId}`,
        content: tweet,
        category: content.category || 'general'
      });
      
      // Keep only last 7 days
      const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
      stats.weeklyPosts = stats.weeklyPosts.filter(p => 
        new Date(p.date).getTime() > sevenDaysAgo
      );
      
      await saveStats(stats);
      
      // Update queue if it was queued content
      if (content.status === 'posting') {
        content.status = 'posted';
        content.postedAt = new Date().toISOString();
        content.tweetId = result.tweetId;
        await fs.writeFile(CONFIG.QUEUE_FILE, JSON.stringify(queue, null, 2));
      }
      
      // Log to posted file
      let posted = [];
      try {
        posted = JSON.parse(await fs.readFile(CONFIG.POSTED_FILE, 'utf8'));
      } catch (e) {
        // File doesn't exist
      }
      posted.push({
        ...content,
        tweet,
        tweetId: result.tweetId,
        url: `https://x.com/DearBubbe/status/${result.tweetId}`,
        postedAt: new Date().toISOString()
      });
      // Keep only last 1000 posts
      if (posted.length > 1000) {
        posted = posted.slice(-1000);
      }
      await fs.writeFile(CONFIG.POSTED_FILE, JSON.stringify(posted, null, 2));
      
      console.log(`✅ Posted successfully! (${stats.postsToday}/${CONFIG.MAX_DAILY_POSTS} today)`);
      console.log(`URL: https://x.com/DearBubbe/status/${result.tweetId}`);
      
      return true;
    } else {
      console.error('❌ Failed to post:', result.error);
      return false;
    }
  } catch (error) {
    console.error('❌ Error in postNext:', error);
    return false;
  }
}

// Generate weekly report
async function generateWeeklyReport() {
  try {
    const stats = await loadStats();
    
    const report = {
      generatedAt: new Date().toISOString(),
      period: {
        start: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString(),
        end: new Date().toISOString()
      },
      summary: {
        totalPosts: stats.weeklyPosts ? stats.weeklyPosts.length : 0,
        dailyAverage: stats.weeklyPosts ? Math.round(stats.weeklyPosts.length / 7) : 0,
        allTimePosts: stats.totalPosts || 0
      },
      dailyBreakdown: {},
      categoryBreakdown: {},
      topPosts: [],
      recommendations: []
    };
    
    if (stats.weeklyPosts && stats.weeklyPosts.length > 0) {
      // Daily breakdown
      for (const post of stats.weeklyPosts) {
        const day = new Date(post.date).toLocaleDateString();
        report.dailyBreakdown[day] = (report.dailyBreakdown[day] || 0) + 1;
      }
      
      // Category breakdown
      for (const post of stats.weeklyPosts) {
        const cat = post.category || 'general';
        report.categoryBreakdown[cat] = (report.categoryBreakdown[cat] || 0) + 1;
      }
      
      // Top posts (most recent for now)
      report.topPosts = stats.weeklyPosts.slice(-10).reverse();
    }
    
    // Recommendations
    if (report.summary.dailyAverage < CONFIG.MAX_DAILY_POSTS) {
      report.recommendations.push(`Increase posting frequency - using only ${report.summary.dailyAverage}/${CONFIG.MAX_DAILY_POSTS} daily allowance`);
    }
    
    if (Object.keys(report.categoryBreakdown).length < 5) {
      report.recommendations.push('Diversify content categories for better engagement');
    }
    
    // Save report
    await fs.writeFile(CONFIG.WEEKLY_REPORT_FILE, JSON.stringify(report, null, 2));
    
    console.log('\n📊 WEEKLY REPORT FOR STEVE');
    console.log('=' .repeat(50));
    console.log(`Period: ${report.period.start.split('T')[0]} to ${report.period.end.split('T')[0]}`);
    console.log(`Total Posts: ${report.summary.totalPosts}`);
    console.log(`Daily Average: ${report.summary.dailyAverage}/${CONFIG.MAX_DAILY_POSTS}`);
    console.log(`All-Time Posts: ${report.summary.allTimePosts}`);
    console.log('\nDaily Breakdown:');
    for (const [day, count] of Object.entries(report.dailyBreakdown)) {
      console.log(`  ${day}: ${count} posts`);
    }
    console.log('\nCategory Breakdown:');
    for (const [cat, count] of Object.entries(report.categoryBreakdown)) {
      console.log(`  ${cat}: ${count} posts`);
    }
    if (report.recommendations.length > 0) {
      console.log('\nRecommendations:');
      report.recommendations.forEach(rec => console.log(`  • ${rec}`));
    }
    console.log('=' .repeat(50));
    
    return report;
  } catch (error) {
    console.error('❌ Error generating report:', error);
    return null;
  }
}

// Main scheduler loop
async function runScheduler() {
  if (postingState.isRunning) {
    console.log('⚠️ Scheduler already running');
    return;
  }
  
  postingState.isRunning = true;
  console.log('🚀 Twitter Scheduler Started');
  console.log(`Max posts per day: ${CONFIG.MAX_DAILY_POSTS}`);
  console.log(`Min interval: ${CONFIG.MIN_INTERVAL_MINUTES} minutes`);
  
  // Main loop
  const scheduleNext = async () => {
    if (!postingState.isRunning) return;
    
    const stats = await loadStats();
    const schedule = calculatePostingSchedule(stats.postsToday);
    
    if (schedule && canPostNow(stats)) {
      await postNext();
    }
    
    // Check if it's time for weekly report (Sunday at midnight)
    const now = new Date();
    if (now.getDay() === 0 && now.getHours() === 0 && now.getMinutes() === 0) {
      await generateWeeklyReport();
    }
    
    // Schedule next check
    const nextCheck = schedule ? schedule * 60 * 1000 : CONFIG.QUEUE_CHECK_INTERVAL;
    setTimeout(scheduleNext, nextCheck);
  };
  
  // Start the loop
  scheduleNext();
}

// Stop scheduler
function stopScheduler() {
  postingState.isRunning = false;
  console.log('🛑 Scheduler stopped');
}

// Export functions
module.exports = {
  runScheduler,
  stopScheduler,
  postNext,
  generateWeeklyReport,
  loadStats,
  generateContent
};

// Run if called directly
if (require.main === module) {
  const command = process.argv[2];
  
  if (command === 'start') {
    runScheduler();
  } else if (command === 'post') {
    postNext().then(() => process.exit(0));
  } else if (command === 'report') {
    generateWeeklyReport().then(() => process.exit(0));
  } else if (command === 'stats') {
    loadStats().then(stats => {
      console.log('📊 Current Stats:');
      console.log(JSON.stringify(stats, null, 2));
      process.exit(0);
    });
  } else {
    console.log('Usage: node twitter-scheduler.js [start|post|report|stats]');
    process.exit(1);
  }
}