← back to Dear Bubbe Nextjs

lib/anonymous-user-poster.js

219 lines

#!/usr/bin/env node

const fs = require('fs').promises;
const path = require('path');
const { postToBluesky } = require('./bluesky-poster');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);

// Anonymization rules
const ANONYMIZE_RULES = {
  // Names to replace
  names: [
    /\b(John|David|Michael|James|Robert|William|Joseph|Thomas|Christopher|Daniel)\b/gi,
    /\b(Mary|Patricia|Jennifer|Linda|Elizabeth|Barbara|Susan|Jessica|Sarah|Karen)\b/gi,
    /\b(Smith|Johnson|Williams|Brown|Jones|Garcia|Miller|Davis|Rodriguez|Martinez)\b/gi
  ],
  nameReplacements: ['someone', 'this person', 'my friend', 'a person I know'],
  
  // Universities to replace
  universities: [
    /\b(Harvard|Yale|Princeton|Stanford|MIT|Columbia|NYU|UCLA|Berkeley|Duke)\b/gi,
    /\b(University of \w+|State University|\w+ State University|\w+ College)\b/gi,
    /\b(community college|university|college|school)\b/gi
  ],
  uniReplacements: ['university', 'college', 'school', 'my school'],
  
  // Personal identifiers to remove
  personalInfo: [
    /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, // Phone numbers
    /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Emails
    /\b\d{3}-\d{2}-\d{4}\b/g, // SSN
    /@[A-Za-z0-9_]+/g, // Social media handles
  ]
};

// Get recent user messages from logs
async function getUserMessages() {
  try {
    // Get last 100 lines from logs and extract user messages
    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();
        // Filter out system messages and very short messages
        if (msg.length > 20 && 
            !msg.includes('__') && 
            !msg.startsWith('test') &&
            !msg.includes('favicon') &&
            !msg.includes('http')) {
          messages.push(msg);
        }
      }
    }
    
    return [...new Set(messages)]; // Remove duplicates
  } catch (error) {
    console.error('Error getting user messages:', error);
    return [];
  }
}

// Anonymize a message
function anonymizeMessage(message) {
  let anonymized = message;
  
  // Replace names
  ANONYMIZE_RULES.names.forEach(pattern => {
    anonymized = anonymized.replace(pattern, () => {
      const replacements = ANONYMIZE_RULES.nameReplacements;
      return replacements[Math.floor(Math.random() * replacements.length)];
    });
  });
  
  // Replace universities
  ANONYMIZE_RULES.universities.forEach(pattern => {
    anonymized = anonymized.replace(pattern, () => {
      const replacements = ANONYMIZE_RULES.uniReplacements;
      return replacements[Math.floor(Math.random() * replacements.length)];
    });
  });
  
  // Remove personal info
  ANONYMIZE_RULES.personalInfo.forEach(pattern => {
    anonymized = anonymized.replace(pattern, '[removed]');
  });
  
  // Additional safety - remove any remaining potential identifiers
  anonymized = anonymized.replace(/\b[A-Z][a-z]+\s[A-Z][a-z]+\b/g, 'someone'); // Potential full names
  anonymized = anonymized.replace(/\b\d{5}(-\d{4})?\b/g, '[zip]'); // Zip codes
  
  return anonymized;
}

// Format post for Bluesky
function formatPost(userMessage) {
  const intros = [
    "Someone just asked Bubbe:",
    "A user needs advice:",
    "Someone wrote to Bubbe:",
    "Real question from today:",
    "Someone's dilemma:",
    "Today's confession:",
    "Anonymous asks Bubbe:"
  ];
  
  const intro = intros[Math.floor(Math.random() * intros.length)];
  const anonymized = anonymizeMessage(userMessage);
  
  // Truncate if too long for Bluesky (300 chars)
  const maxLength = 250; // Leave room for intro and link
  const truncated = anonymized.length > maxLength ? 
    anonymized.substring(0, maxLength) + '...' : 
    anonymized;
  
  return `${intro}\n"${truncated}"\n\nWhat would Bubbe say? Find out at bubbe.ai`;
}

// Load state
async function loadState() {
  const stateFile = '/root/Projects/dear-bubbe-admin/logs/anonymous-posts-state.json';
  try {
    const data = await fs.readFile(stateFile, 'utf8');
    return JSON.parse(data);
  } catch {
    return {
      postedMessages: [],
      lastPostTime: null,
      totalPosts: 0
    };
  }
}

// Save state
async function saveState(state) {
  const stateFile = '/root/Projects/dear-bubbe-admin/logs/anonymous-posts-state.json';
  await fs.writeFile(stateFile, JSON.stringify(state, null, 2));
}

// Main posting function
async function postAnonymousUserContent() {
  console.log('🔒 Anonymous User Content Poster');
  console.log('================================');
  
  // Load state
  const state = await loadState();
  
  // Check if we should wait (minimum 30 minutes between posts)
  if (state.lastPostTime) {
    const timeSince = Date.now() - new Date(state.lastPostTime).getTime();
    const thirtyMinutes = 30 * 60 * 1000;
    if (timeSince < thirtyMinutes) {
      const waitTime = Math.round((thirtyMinutes - 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 messages
  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 unposted message
  const selectedMessage = unposted[Math.floor(Math.random() * unposted.length)];
  console.log(`\n🎯 Selected message: "${selectedMessage.substring(0, 50)}..."`);
  
  // Format and anonymize
  const post = formatPost(selectedMessage);
  console.log(`\n🔒 Anonymized post:\n${post}\n`);
  
  // Post to Bluesky
  try {
    const result = await postToBluesky(post);
    if (result.success) {
      console.log('✅ Posted successfully!');
      console.log(`🔗 ${result.url}`);
      
      // Update state
      state.postedMessages.push(selectedMessage);
      state.lastPostTime = new Date().toISOString();
      state.totalPosts++;
      
      // Keep only last 100 posted messages to avoid huge state file
      if (state.postedMessages.length > 100) {
        state.postedMessages = state.postedMessages.slice(-100);
      }
      
      await saveState(state);
      console.log(`📊 Total anonymous posts: ${state.totalPosts}`);
    } else {
      console.error('❌ Failed to post:', result.error);
    }
  } catch (error) {
    console.error('❌ Error posting:', error);
  }
}

// Run if called directly
if (require.main === module) {
  postAnonymousUserContent().catch(console.error);
}

module.exports = { postAnonymousUserContent, anonymizeMessage };