← back to Dear Bubbe Nextjs

lib/social-autoposter.js

149 lines

#!/usr/bin/env node

/**
 * Unified Social Media Auto-Poster for Dear Bubbe
 * Posts to Twitter/X and Bluesky using official APIs
 *
 * Usage: node social-autoposter.js [twitter|bluesky|all] [post|schedule|stats]
 */

const { postTweet } = require('./twitter-v2-official');
const { optimizeTweet } = require('./twitter-optimizer');
const { postToBluesky } = require('./bluesky-poster');
const { anonymizeMessage } = require('./anonymous-user-poster');
const fs = require('fs').promises;
const axios = require('axios');

const STATE_FILE = '/root/Projects/dear-bubbe-admin/logs/social-poster-state.json';
const LOG_FILE = '/root/Projects/dear-bubbe-nextjs/logs/social-posts.log';

const CONFIG = {
  twitter: { maxDaily: 100, minIntervalMin: 3, peakHours: [7, 9, 12, 15, 17, 19, 21] },
  bluesky: { maxDaily: 10, minIntervalMin: 60 }
};

async function loadState() {
  try {
    const state = JSON.parse(await fs.readFile(STATE_FILE, 'utf8'));
    const today = new Date().toDateString();
    if (state.currentDay !== today) {
      state.twitter = { postsToday: 0, lastPostTime: null };
      state.bluesky = { postsToday: 0, lastPostTime: null };
      state.currentDay = today;
    }
    return state;
  } catch {
    return {
      currentDay: new Date().toDateString(),
      twitter: { postsToday: 0, lastPostTime: null },
      bluesky: { postsToday: 0, lastPostTime: null },
      totalPosts: 0, weeklyPosts: []
    };
  }
}

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

function canPost(state, platform) {
  const cfg = CONFIG[platform];
  const plat = state[platform];
  if (plat.postsToday >= cfg.maxDaily) return false;
  if (plat.lastPostTime) {
    const elapsed = Date.now() - new Date(plat.lastPostTime).getTime();
    if (elapsed < cfg.minIntervalMin * 60000) return false;
  }
  return true;
}

async function generateContent() {
  const topics = [
    "My mother-in-law criticized my cooking again",
    "Should I quit my job without another lined up?",
    "My boyfriend only texts me once a day",
    "I spent $500 on shoes, was that too much?",
    "My kids won't call me back",
    "Is 35 too old to start dating again?",
    "My boss is a total micromanager",
    "I haven't been to temple in years",
    "My friend is jealous of my success",
    "I'm 28 and still single, is something wrong with me?"
  ];
  const topic = topics[Math.floor(Math.random() * topics.length)];
  try {
    const resp = await axios.post('http://45.61.58.125:3011/api/chat', {
      message: topic, mode: 'bubbe', userId: 'poster-' + Date.now(), location: 'New York, NY'
    });
    if (resp.data?.response) {
      return { userMessage: anonymizeMessage(topic), bubbeResponse: resp.data.response };
    }
  } catch (e) { console.error('Content gen failed:', e.message); }
  return null;
}

async function postTo(platform, content) {
  const state = await loadState();
  if (!canPost(state, platform)) {
    console.log(`Rate limited on ${platform} (${state[platform].postsToday}/${CONFIG[platform].maxDaily})`);
    return false;
  }

  const text = platform === 'twitter'
    ? optimizeTweet(content.userMessage, content.bubbeResponse)
    : `Bubbe says:\n"${content.bubbeResponse.substring(0, 250)}"\n\nVisit Bubbe.AI #BubbeAI #JewishGrandma`;

  let result;
  if (platform === 'twitter') {
    result = await postTweet(text);
  } else {
    result = await postToBluesky(text);
  }

  if (result.success) {
    state[platform].postsToday++;
    state[platform].lastPostTime = new Date().toISOString();
    state.totalPosts++;
    state.weeklyPosts = (state.weeklyPosts || []).concat({
      date: new Date().toISOString(), platform, text: text.substring(0, 100)
    }).slice(-500);
    await saveState(state);
    console.log(`Posted to ${platform} (#${state[platform].postsToday}/${CONFIG[platform].maxDaily})`);
  }
  return result.success;
}

async function postToAll() {
  const content = await generateContent();
  if (!content) { console.log('No content generated'); return; }
  const tw = await postTo('twitter', content).catch(() => false);
  await new Promise(r => setTimeout(r, 3000));
  const bs = await postTo('bluesky', content).catch(() => false);
  console.log(`Results: Twitter=${tw}, Bluesky=${bs}`);
}

async function runScheduler(platform) {
  console.log(`Scheduler started for ${platform || 'all'}`);
  const loop = async () => {
    try {
      if (!platform || platform === 'all') await postToAll();
      else {
        const content = await generateContent();
        if (content) await postTo(platform, content);
      }
    } catch (e) { console.error('Scheduler error:', e.message); }
    const delay = (platform === 'bluesky') ? 3600000 : 180000;
    setTimeout(loop, delay);
  };
  loop();
}

if (require.main === module) {
  const [platform = 'all', cmd = 'post'] = process.argv.slice(2);
  if (cmd === 'schedule') runScheduler(platform);
  else if (cmd === 'stats') loadState().then(s => { console.log(JSON.stringify(s, null, 2)); process.exit(0); });
  else postToAll().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
}

module.exports = { postTo, postToAll, runScheduler, generateContent, loadState };