← back to Dear Bubbe Nextjs

archived/social-posters/smart-twitter-scheduler.js

330 lines

#!/usr/bin/env node

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

// Configuration
const CONFIG = {
  MAX_DAILY_POSTS: 100,
  MIN_INTERVAL_SECONDS: 180, // 3 minutes minimum
  STATS_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-stats.json',
  LAST_POST_FILE: '/root/Projects/dear-bubbe-admin/logs/last-twitter-post.json'
};

// Viral hashtags that work
const HASHTAGS = {
  always: ['#BubbeAI', '#Bubbe'],
  viral: ['#JewishTwitter', '#Dating', '#Relationships', '#MondayMotivation', '#Funny', '#Comedy', '#NoFilter', '#Truth', '#Savage', '#Life'],
  day: {
    0: '#SundayFunday',
    1: '#MondayMotivation', 
    2: '#TuesdayThoughts',
    3: '#WednesdayWisdom',
    4: '#ThursdayThoughts',
    5: '#FridayFeeling',
    6: '#SaturdayVibes'
  }
};

// Content templates
const CONTENT = [
  "My boyfriend only texts once a day",
  "Should I quit my job without another lined up?",
  "My mother-in-law criticized my cooking",
  "I spent $500 on shoes",
  "Is 35 too old to start dating?",
  "My husband forgot our anniversary",
  "Should I text my ex?",
  "I haven't been to temple in years",
  "My kids never call me",
  "My boss is micromanaging me",
  "I'm thinking about getting married",
  "My friend owes me money",
  "Should I move back home?",
  "I hate my job but it pays well",
  "My partner doesn't help with chores",
  "I'm 30 and still single",
  "My parents don't approve of my boyfriend",
  "Should I get a dog?",
  "I lied to my best friend",
  "My neighbor is too noisy"
];

// Calculate smart posting interval
function calculateNextInterval(postsToday, hoursLeft) {
  const postsRemaining = CONFIG.MAX_DAILY_POSTS - postsToday;
  
  if (postsRemaining <= 0) return null;
  if (hoursLeft <= 0) return CONFIG.MIN_INTERVAL_SECONDS;
  
  const now = new Date();
  const hour = now.getHours();
  const minute = now.getMinutes();
  
  // Peak engagement hours (more frequent posting)
  const peakHours = {
    7: 5,   // 7am - morning commute
    8: 5,   // 8am
    9: 4,   // 9am - work start
    12: 4,  // noon - lunch
    13: 4,  // 1pm
    17: 4,  // 5pm - end of work
    18: 5,  // 6pm - commute home
    19: 5,  // 7pm - dinner
    20: 4,  // 8pm - evening
    21: 4   // 9pm - before bed
  };
  
  // Off-peak hours (less frequent)
  const offPeakHours = {
    0: 1,   // midnight
    1: 1,   // 1am
    2: 1,   // 2am
    3: 1,   // 3am
    4: 1,   // 4am
    5: 2,   // 5am
    6: 3,   // 6am
    10: 3,  // 10am
    11: 3,  // 11am
    14: 3,  // 2pm
    15: 3,  // 3pm
    16: 3,  // 4pm
    22: 2,  // 10pm
    23: 2   // 11pm
  };
  
  // Get target posts for this hour
  let targetPostsThisHour = offPeakHours[hour] || 3;
  if (peakHours[hour]) {
    targetPostsThisHour = peakHours[hour];
  }
  
  // Calculate how many minutes between posts this hour
  const minutesLeftInHour = 60 - minute;
  let intervalMinutes = Math.floor(minutesLeftInHour / targetPostsThisHour);
  
  // Ensure we hit our daily target
  const minutesLeftToday = hoursLeft * 60;
  const neededInterval = Math.floor(minutesLeftToday / postsRemaining);
  
  // Use the smaller interval to ensure we use all posts
  intervalMinutes = Math.min(intervalMinutes, neededInterval);
  
  // Add some randomness (±20%)
  const variance = Math.floor(intervalMinutes * 0.2);
  intervalMinutes += Math.floor(Math.random() * (variance * 2 + 1) - variance);
  
  // Enforce minimum interval
  intervalMinutes = Math.max(3, intervalMinutes); // 3 minutes minimum
  
  console.log(`📊 Scheduling: ${postsRemaining} posts left, ${hoursLeft.toFixed(1)}h remaining`);
  console.log(`⏱️  Next post in ${intervalMinutes} minutes`);
  
  return intervalMinutes * 60; // Return in seconds
}

// Get Bubbe response
async function getBubbeResponse(message) {
  try {
    const response = await axios.post('http://45.61.58.125:3011/api/chat', {
      message: message,
      mode: 'bubbe',
      userId: 'twitter-bot-' + Date.now()
    });
    return response.data.response;
  } catch (error) {
    console.error('Failed to get Bubbe response:', error.message);
    return null;
  }
}

// Format tweet - clean, no ellipsis
function formatTweet(response) {
  const { formatCleanTweet } = require('./twitter-clean-formatter');
  return formatCleanTweet('', response);
}

// Check if can post
async function canPost() {
  try {
    // Check daily limit
    const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
    const today = new Date().toDateString();
    
    if (stats.date !== today) {
      stats.date = today;
      stats.count = 0;
    }
    
    if (stats.count >= CONFIG.MAX_DAILY_POSTS) {
      console.log(`Daily limit reached: ${stats.count}/${CONFIG.MAX_DAILY_POSTS}`);
      return false;
    }
    
    // Check time since last post
    const lastPost = JSON.parse(await fs.readFile(CONFIG.LAST_POST_FILE, 'utf8').catch(() => '{}'));
    if (lastPost.time) {
      const elapsed = (Date.now() - lastPost.time) / 1000;
      if (elapsed < CONFIG.MIN_INTERVAL_SECONDS) {
        console.log(`Rate limit: wait ${Math.ceil(CONFIG.MIN_INTERVAL_SECONDS - elapsed)}s`);
        return false;
      }
    }
    
    return true;
  } catch (error) {
    console.error('Error checking limits:', error);
    return false;
  }
}

// Update stats
async function updateStats() {
  try {
    const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
    const today = new Date().toDateString();
    
    if (stats.date !== today) {
      stats.date = today;
      stats.count = 0;
    }
    
    stats.count++;
    stats.total = (stats.total || 0) + 1;
    
    await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
    await fs.writeFile(CONFIG.LAST_POST_FILE, JSON.stringify({ time: Date.now() }, null, 2));
    
    return stats;
  } catch (error) {
    console.error('Error updating stats:', error);
  }
}

// Post one tweet
async function postOne() {
  if (!await canPost()) {
    return false;
  }
  
  // Get random content
  const topic = CONTENT[Math.floor(Math.random() * CONTENT.length)];
  console.log('📝 Topic:', topic);
  
  // Get Bubbe response
  const response = await getBubbeResponse(topic);
  if (!response) {
    console.log('No response from Bubbe');
    return false;
  }
  
  // Format and post
  const tweet = formatTweet(response);
  console.log('🐦 Tweet:', tweet.substring(0, 100) + '...');
  console.log('📏 Length:', tweet.length);
  
  const result = await postTweet(tweet);
  
  if (result.success) {
    const stats = await updateStats();
    console.log(`✅ Posted! (${stats.count}/${CONFIG.MAX_DAILY_POSTS} today, ${stats.total} total)`);
    console.log(`🔗 URL: https://x.com/DearBubbe/status/${result.tweetId}`);
    return true;
  } else {
    console.log('❌ Failed:', result.error);
    return false;
  }
}

// Run smart scheduler
async function runSmartScheduler() {
  console.log('🚀 Smart Twitter Scheduler Started');
  console.log(`📊 Goal: ${CONFIG.MAX_DAILY_POSTS} posts per day`);
  console.log(`⏱️  Min interval: ${CONFIG.MIN_INTERVAL_SECONDS}s (3 minutes)`);
  console.log('');
  
  while (true) {
    try {
      // Get current stats
      const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
      const today = new Date().toDateString();
      
      if (stats.date !== today) {
        stats.date = today;
        stats.count = 0;
        await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
      }
      
      // Calculate time remaining today
      const now = new Date();
      const endOfDay = new Date();
      endOfDay.setHours(23, 59, 59, 999);
      const hoursLeft = (endOfDay - now) / (1000 * 60 * 60);
      
      // Post if we can
      if (await canPost()) {
        await postOne();
      }
      
      // Calculate next interval
      const nextInterval = calculateNextInterval(stats.count || 0, hoursLeft);
      
      if (!nextInterval) {
        console.log('✅ Daily goal reached! Waiting for tomorrow...');
        // Wait until midnight
        const msUntilMidnight = endOfDay - new Date() + 1000;
        await new Promise(r => setTimeout(r, msUntilMidnight));
      } else {
        // Wait for next post
        await new Promise(r => setTimeout(r, nextInterval * 1000));
      }
    } catch (error) {
      console.error('❌ Scheduler error:', error);
      // Wait 5 minutes on error
      await new Promise(r => setTimeout(r, 5 * 60 * 1000));
    }
  }
}

// Get status
async function getStatus() {
  const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
  const now = new Date();
  const endOfDay = new Date();
  endOfDay.setHours(23, 59, 59, 999);
  const hoursLeft = (endOfDay - now) / (1000 * 60 * 60);
  const postsRemaining = CONFIG.MAX_DAILY_POSTS - (stats.count || 0);
  
  console.log('\n📊 TWITTER SCHEDULER STATUS');
  console.log('=' .repeat(40));
  console.log(`Today: ${now.toDateString()}`);
  console.log(`Posts today: ${stats.count || 0}/${CONFIG.MAX_DAILY_POSTS}`);
  console.log(`Posts remaining: ${postsRemaining}`);
  console.log(`Hours left today: ${hoursLeft.toFixed(1)}`);
  
  if (postsRemaining > 0 && hoursLeft > 0) {
    const avgInterval = (hoursLeft * 60) / postsRemaining;
    console.log(`Average interval needed: ${avgInterval.toFixed(1)} minutes`);
    
    if (avgInterval < 3) {
      console.log('⚠️  WARNING: May not reach daily goal!');
    } else {
      console.log('✅ On track to reach daily goal');
    }
  }
  
  console.log('=' .repeat(40));
}

// Start
if (require.main === module) {
  const command = process.argv[2];
  
  if (command === 'status') {
    getStatus().then(() => process.exit(0));
  } else {
    runSmartScheduler().catch(console.error);
  }
}