← back to Dear Bubbe Nextjs
archived/social-posters/twitter-anonymous-poster.js
292 lines
#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const playwright = require('playwright');
// Import anonymization from Bluesky poster
const { anonymizeMessage } = require('./anonymous-user-poster');
// Configuration
const CONFIG = {
TWITTER_USERNAME: 'DearBubbe',
POSTS_PER_DAY: 20,
MIN_INTERVAL_MINUTES: 30,
STATE_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-anonymous-state.json',
CREDENTIALS_FILE: '/root/Projects/dear-bubbe-nextjs/lib/twitter-cookies.json'
};
// Get recent user messages from logs
async function getUserMessages() {
try {
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();
if (msg.length > 20 &&
!msg.includes('__') &&
!msg.startsWith('test') &&
!msg.includes('favicon') &&
!msg.includes('http')) {
messages.push(msg);
}
}
}
return [...new Set(messages)];
} catch (error) {
console.error('Error getting user messages:', error);
return [];
}
}
// Format post for Twitter
function formatPostForTwitter(userMessage) {
const intros = [
"Someone just asked Bubbe:",
"Real question today:",
"Anonymous asks:",
"User dilemma:",
"Today's confession:",
"Someone needs advice:"
];
const intro = intros[Math.floor(Math.random() * intros.length)];
const anonymized = anonymizeMessage(userMessage);
// Twitter has 280 char limit
const maxLength = 200; // Leave room for intro, hashtags, and link
const truncated = anonymized.length > maxLength ?
anonymized.substring(0, maxLength) + '...' :
anonymized;
const hashtags = '#BubbeAI #JewishGrandma #Advice';
return `${intro}\n"${truncated}"\n\n${hashtags}\nbubbe.ai`;
}
// Load state
async function loadState() {
try {
const data = await fs.readFile(CONFIG.STATE_FILE, 'utf8');
return JSON.parse(data);
} catch {
return {
postedMessages: [],
lastPostTime: null,
totalPosts: 0,
dailyPosts: 0,
lastResetDate: new Date().toDateString()
};
}
}
// Save state
async function saveState(state) {
await fs.writeFile(CONFIG.STATE_FILE, JSON.stringify(state, null, 2));
}
// Post to Twitter using Playwright
async function postToTwitter(text) {
const browser = await playwright.chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
viewport: { width: 1280, height: 720 }
});
// Load cookies if available
try {
const cookies = JSON.parse(await fs.readFile(CONFIG.CREDENTIALS_FILE, 'utf8'));
if (cookies && cookies.length > 0) {
await context.addCookies(cookies);
}
} catch (e) {
console.log('⚠️ No saved cookies, will need to login');
}
const page = await context.newPage();
// Go to Twitter
await page.goto('https://x.com/compose/post', { waitUntil: 'networkidle', timeout: 30000 });
// Wait for compose box
await page.waitForTimeout(3000);
// Check if we need to login
const needsLogin = await page.$('input[autocomplete="username"]');
if (needsLogin) {
console.log('❌ Twitter login required - manual intervention needed');
return { success: false, error: 'Login required' };
}
// Find the compose box and type
const composeBox = await page.$('[data-testid="tweetTextarea_0"]');
if (!composeBox) {
console.log('❌ Could not find compose box');
return { success: false, error: 'Compose box not found' };
}
// Type the message
await composeBox.type(text, { delay: 50 });
await page.waitForTimeout(1000);
// Click Post button
const postButton = await page.$('[data-testid="tweetButtonInline"]');
if (!postButton) {
console.log('❌ Could not find post button');
return { success: false, error: 'Post button not found' };
}
await postButton.click();
await page.waitForTimeout(3000);
// Save cookies for next time
const cookies = await context.cookies();
await fs.writeFile(CONFIG.CREDENTIALS_FILE, JSON.stringify(cookies, null, 2));
console.log('✅ Posted to Twitter successfully');
return { success: true };
} catch (error) {
console.error('❌ Error posting to Twitter:', error);
return { success: false, error: error.message };
} finally {
await browser.close();
}
}
// Main posting function
async function postAnonymousToTwitter() {
console.log('🐦 Twitter Anonymous User Content Poster');
console.log('========================================');
// Load state
const state = await loadState();
// Reset daily counter if needed
const today = new Date().toDateString();
if (today !== state.lastResetDate) {
state.dailyPosts = 0;
state.lastResetDate = today;
console.log('📅 New day - reset counter');
}
// Check daily limit
if (state.dailyPosts >= CONFIG.POSTS_PER_DAY) {
console.log(`📊 Daily limit reached (${state.dailyPosts}/${CONFIG.POSTS_PER_DAY})`);
return;
}
// Check minimum interval
if (state.lastPostTime) {
const timeSince = Date.now() - new Date(state.lastPostTime).getTime();
const minInterval = CONFIG.MIN_INTERVAL_MINUTES * 60 * 1000;
if (timeSince < minInterval) {
const waitTime = Math.round((minInterval - 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
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 message
const selectedMessage = unposted[Math.floor(Math.random() * unposted.length)];
console.log(`\n🎯 Selected: "${selectedMessage.substring(0, 50)}..."`);
// Format for Twitter
const post = formatPostForTwitter(selectedMessage);
console.log(`\n🐦 Tweet:\n${post}\n`);
// Post to Twitter
const result = await postToTwitter(post);
if (result.success) {
// Update state
state.postedMessages.push(selectedMessage);
state.lastPostTime = new Date().toISOString();
state.totalPosts++;
state.dailyPosts++;
// Keep only last 100 messages
if (state.postedMessages.length > 100) {
state.postedMessages = state.postedMessages.slice(-100);
}
await saveState(state);
console.log(`📊 Posted ${state.dailyPosts}/${CONFIG.POSTS_PER_DAY} today`);
console.log(`📈 Total posts: ${state.totalPosts}`);
} else {
console.error('❌ Failed to post:', result.error);
// If login required, provide fallback
if (result.error === 'Login required') {
console.log('\n📋 MANUAL POST:');
console.log('1. Go to: https://x.com/compose/post');
console.log('2. Login if needed');
console.log('3. Copy and paste:');
console.log('-'.repeat(40));
console.log(post);
console.log('-'.repeat(40));
}
}
}
// Continuous posting loop
async function continuousPost() {
console.log('🚀 Starting Twitter Continuous Anonymous Poster');
console.log(`⏱️ Posting every ${CONFIG.MIN_INTERVAL_MINUTES} minutes`);
console.log(`📊 Max ${CONFIG.POSTS_PER_DAY} posts per day`);
console.log('=' .repeat(50));
// Post immediately
await postAnonymousToTwitter();
// Continue at intervals
setInterval(async () => {
await postAnonymousToTwitter();
}, CONFIG.MIN_INTERVAL_MINUTES * 60 * 1000);
}
// Handle shutdown
process.on('SIGINT', () => {
console.log('\n👋 Shutting down Twitter poster...');
process.exit(0);
});
// Run if called directly
if (require.main === module) {
const args = process.argv.slice(2);
if (args[0] === 'continuous') {
continuousPost().catch(console.error);
} else {
postAnonymousToTwitter().catch(console.error);
}
}
module.exports = { postAnonymousToTwitter };