← 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 };