← back to Dear Bubbe Nextjs

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

251 lines

#!/usr/bin/env node

const fs = require('fs').promises;
const path = require('path');
const axios = require('axios');
const { postToBluesky } = require('./bluesky-poster');

// Configuration
const CONFIG = {
  MAX_DAILY_POSTS: 3,  // 3 posts per day as requested
  MIN_INTERVAL_SECONDS: 14400, // 4 hours minimum between posts
  STATS_FILE: '/root/Projects/dear-bubbe-admin/logs/bluesky-stats.json',
  LAST_POST_FILE: '/root/Projects/dear-bubbe-admin/logs/last-bluesky-post.json'
};

// Content templates
const CONTENT = [
  "My boyfriend only texts once a day",
  "Should I quit my job?",
  "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"
];

// 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();
  
  // Peak hours for Bluesky (different audience than Twitter)
  const peakHours = {
    8: 5,   // 8am
    9: 4,   // 9am
    12: 4,  // noon
    17: 4,  // 5pm
    18: 5,  // 6pm
    19: 5,  // 7pm
    20: 4,  // 8pm
    21: 4   // 9pm
  };
  
  // Get target posts for this hour
  let targetPostsThisHour = peakHours[hour] || 3;
  
  // Calculate interval
  const minutesLeftInHour = 60 - now.getMinutes();
  let intervalMinutes = Math.floor(minutesLeftInHour / targetPostsThisHour);
  
  // Ensure we hit daily target
  const minutesLeftToday = hoursLeft * 60;
  const neededInterval = Math.floor(minutesLeftToday / postsRemaining);
  
  intervalMinutes = Math.min(intervalMinutes, neededInterval);
  
  // Add randomness
  const variance = Math.floor(intervalMinutes * 0.2);
  intervalMinutes += Math.floor(Math.random() * (variance * 2 + 1) - variance);
  
  // Enforce minimum
  intervalMinutes = Math.max(3, intervalMinutes);
  
  return intervalMinutes * 60;
}

// 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: 'bluesky-bot-' + Date.now()
    });
    return response.data.response;
  } catch (error) {
    console.error('Failed to get Bubbe response:', error.message);
    return null;
  }
}

// Format for Bluesky - clean, complete sentences
function formatForBluesky(response) {
  // Clean response
  let clean = response
    .replace(/\s+/g, ' ')
    .trim();
  
  // Get complete sentences
  const sentences = clean.match(/[^.!?]+[.!?]+/g) || [clean];
  
  const maxLength = 250; // Leave room for hashtags
  let post = '';
  
  for (const sentence of sentences) {
    const trimmed = sentence.trim();
    if (post.length + trimmed.length + 1 <= maxLength) {
      post += (post ? ' ' : '') + trimmed;
    } else {
      break;
    }
  }
  
  // Ensure it ends with punctuation
  if (post && !post.match(/[.!?]$/)) {
    post += '!';
  }
  
  return `${post} #BubbeAI #Bubbe #JewishGrandma Visit Bubbe.AI`;
}

// Check if can post
async function canPost() {
  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;
    }
    
    if (stats.count >= CONFIG.MAX_DAILY_POSTS) {
      console.log(`Daily limit reached: ${stats.count}/${CONFIG.MAX_DAILY_POSTS}`);
      return false;
    }
    
    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
async function postOne() {
  if (!await canPost()) {
    return false;
  }
  
  const topic = CONTENT[Math.floor(Math.random() * CONTENT.length)];
  console.log('📝 Topic:', topic);
  
  const response = await getBubbeResponse(topic);
  if (!response) {
    console.log('No response from Bubbe');
    return false;
  }
  
  const post = formatForBluesky(response);
  console.log('🦋 Post:', post.substring(0, 100) + '...');
  
  const result = await postToBluesky(post);
  
  if (result.success) {
    const stats = await updateStats();
    console.log(`✅ Posted to Bluesky! (${stats.count}/${CONFIG.MAX_DAILY_POSTS} today)`);
    console.log(`🔗 ${result.url}`);
    return true;
  } else {
    console.log('❌ Failed:', result.error);
    return false;
  }
}

// Run scheduler
async function runScheduler() {
  console.log('🦋 Bluesky Smart Scheduler Started');
  console.log(`📊 Goal: ${CONFIG.MAX_DAILY_POSTS} posts per day`);
  console.log(`⏱️  Min interval: ${CONFIG.MIN_INTERVAL_SECONDS}s`);
  
  while (true) {
    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;
        await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
      }
      
      const now = new Date();
      const endOfDay = new Date();
      endOfDay.setHours(23, 59, 59, 999);
      const hoursLeft = (endOfDay - now) / (1000 * 60 * 60);
      
      if (await canPost()) {
        await postOne();
      }
      
      const nextInterval = calculateNextInterval(stats.count || 0, hoursLeft);
      
      if (!nextInterval) {
        console.log('✅ Daily goal reached!');
        const msUntilMidnight = endOfDay - new Date() + 1000;
        await new Promise(r => setTimeout(r, msUntilMidnight));
      } else {
        console.log(`⏱️  Next post in ${Math.round(nextInterval/60)} minutes`);
        await new Promise(r => setTimeout(r, nextInterval * 1000));
      }
    } catch (error) {
      console.error('❌ Error:', error);
      await new Promise(r => setTimeout(r, 5 * 60 * 1000));
    }
  }
}

if (require.main === module) {
  runScheduler().catch(console.error);
}