← back to Dear Bubbe Nextjs
archived/social-posters/twitter-scheduler.js
445 lines
const fs = require('fs').promises;
const path = require('path');
const { postTweet } = require('./twitter-v2-official');
const { optimizeTweet } = require('./twitter-optimizer');
const axios = require('axios');
// Configuration
const CONFIG = {
MAX_DAILY_POSTS: 100,
MIN_INTERVAL_MINUTES: 3,
QUEUE_CHECK_INTERVAL: 60000, // Check every minute
STATS_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-stats.json',
QUEUE_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-queue.json',
POSTED_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-posted.json',
WEEKLY_REPORT_FILE: '/root/Projects/dear-bubbe-admin/logs/weekly-twitter-report.json'
};
// Track posting state
let postingState = {
lastPostTime: null,
postsToday: 0,
dayStartTime: null,
isRunning: false
};
// Load or initialize stats
async function loadStats() {
try {
const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8'));
// Check if it's a new day
const today = new Date().toDateString();
if (stats.currentDay !== today) {
stats.postsToday = 0;
stats.currentDay = today;
stats.dayStartTime = new Date().toISOString();
}
return stats;
} catch (e) {
// Initialize new stats
return {
postsToday: 0,
currentDay: new Date().toDateString(),
dayStartTime: new Date().toISOString(),
lastPostTime: null,
totalPosts: 0,
weeklyPosts: [],
startDate: new Date().toISOString()
};
}
}
// Save stats
async function saveStats(stats) {
await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
}
// Check if we can post now (rate limiting)
function canPostNow(stats) {
// Check daily limit
if (stats.postsToday >= CONFIG.MAX_DAILY_POSTS) {
console.log(`📊 Daily limit reached: ${stats.postsToday}/${CONFIG.MAX_DAILY_POSTS}`);
return false;
}
// Check minimum interval
if (stats.lastPostTime) {
const timeSinceLastPost = Date.now() - new Date(stats.lastPostTime).getTime();
const minInterval = CONFIG.MIN_INTERVAL_MINUTES * 60 * 1000;
if (timeSinceLastPost < minInterval) {
const waitTime = Math.ceil((minInterval - timeSinceLastPost) / 1000);
console.log(`⏱️ Rate limit: Wait ${waitTime}s before next post`);
return false;
}
}
return true;
}
// Calculate optimal posting schedule
function calculatePostingSchedule(postsToday) {
const now = new Date();
const hour = now.getHours();
const remainingToday = CONFIG.MAX_DAILY_POSTS - postsToday;
if (remainingToday <= 0) return null;
// Calculate hours remaining in day
const hoursLeft = 24 - hour;
// Optimal posting times (ET) - peak engagement
const peakHours = [7, 9, 12, 15, 17, 19, 21]; // 7am, 9am, 12pm, 3pm, 5pm, 7pm, 9pm
// If we're in a peak hour, post more frequently
if (peakHours.includes(hour)) {
return 3; // Post every 3 minutes during peak
}
// Otherwise, spread remaining posts throughout the day
if (hoursLeft > 0) {
const postsPerHour = Math.ceil(remainingToday / hoursLeft);
const minutesBetweenPosts = Math.max(CONFIG.MIN_INTERVAL_MINUTES, Math.floor(60 / postsPerHour));
return minutesBetweenPosts;
}
return CONFIG.MIN_INTERVAL_MINUTES;
}
// Generate content when queue is empty
async function generateContent() {
console.log('🎨 Generating fresh content...');
// Topics that get engagement
const topics = [
{ user: "My mother-in-law criticized my cooking again", category: "family" },
{ user: "Should I quit my job without another lined up?", category: "career" },
{ user: "My boyfriend only texts me once a day", category: "dating" },
{ user: "I spent $500 on shoes, was that too much?", category: "money" },
{ user: "My kids won't call me back", category: "family" },
{ user: "Is 35 too old to start dating again?", category: "dating" },
{ user: "My boss is a total micromanager", category: "work" },
{ user: "Should I get married or stay single?", category: "relationships" },
{ user: "I haven't been to temple in years", category: "jewish" },
{ user: "My neighbor's dog won't stop barking", category: "life" },
{ user: "I think my friend is jealous of my success", category: "friendship" },
{ user: "Should I move back in with my parents to save money?", category: "adulting" },
{ user: "I'm 28 and still single, is something wrong with me?", category: "dating" },
{ user: "My husband forgot our anniversary", category: "marriage" },
{ user: "I hate my job but the pay is good", category: "career" },
{ user: "My adult children still ask me for money", category: "parenting" },
{ user: "Should I go on a second date if there's no spark?", category: "dating" },
{ user: "I lied to my partner about something small", category: "relationships" },
{ user: "Is it wrong to not want kids?", category: "life" },
{ user: "My best friend is getting divorced", category: "friendship" }
];
// Pick random topic
const topic = topics[Math.floor(Math.random() * topics.length)];
// Generate Bubbe response using Claude
try {
const response = await axios.post('http://45.61.58.125:3011/api/chat', {
message: topic.user,
mode: 'bubbe',
userId: 'scheduler-' + Date.now(),
location: 'New York, NY'
});
if (response.data && response.data.response) {
return {
userMessage: topic.user,
bubbeResponse: response.data.response,
category: topic.category,
generated: true,
timestamp: new Date().toISOString()
};
}
} catch (error) {
console.error('❌ Failed to generate content:', error.message);
}
// Fallback to pre-written responses
const fallbacks = [
{
userMessage: "My mother thinks I'm wasting my life",
bubbeResponse: "Oy vey, maybe she's right! What are you doing with yourself? No spouse, no kids, no real job? Listen bubbeleh, mothers know. Get your act together before it's too late!",
category: "family"
},
{
userMessage: "Should I text my ex?",
bubbeResponse: "Are you meshuga? NO! What are you, a glutton for punishment? They're an ex for a reason! Delete their number and find someone who deserves you, schmuck!",
category: "dating"
},
{
userMessage: "I'm thinking about getting a tattoo",
bubbeResponse: "A tattoo? Oy gevalt! You know that's permanent, right? What will you tell your grandchildren? 'Oh, this blob used to be a butterfly?' Think before you ink, bubbeleh!",
category: "life"
}
];
return fallbacks[Math.floor(Math.random() * fallbacks.length)];
}
// Post from queue or generate new content
async function postNext() {
const stats = await loadStats();
if (!canPostNow(stats)) {
return false;
}
try {
// Check queue first
let queue = [];
try {
queue = JSON.parse(await fs.readFile(CONFIG.QUEUE_FILE, 'utf8'));
} catch (e) {
// No queue file
}
// Filter unposted items
const unposted = queue.filter(item => item.status !== 'posted');
let content;
if (unposted.length > 0) {
// Use queued content
content = unposted[0];
content.status = 'posting';
} else {
// Generate new content
content = await generateContent();
if (!content) {
console.log('⚠️ No content to post');
return false;
}
}
// Optimize and post
const tweet = optimizeTweet(content.userMessage, content.bubbeResponse);
console.log(`\n📤 Posting tweet #${stats.postsToday + 1}/${CONFIG.MAX_DAILY_POSTS}`);
console.log(`Tweet: ${tweet.substring(0, 100)}...`);
const result = await postTweet(tweet);
if (result.success) {
// Update stats
stats.postsToday++;
stats.totalPosts++;
stats.lastPostTime = new Date().toISOString();
// Add to weekly tracking
if (!stats.weeklyPosts) stats.weeklyPosts = [];
stats.weeklyPosts.push({
date: new Date().toISOString(),
tweetId: result.tweetId,
url: `https://x.com/DearBubbe/status/${result.tweetId}`,
content: tweet,
category: content.category || 'general'
});
// Keep only last 7 days
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
stats.weeklyPosts = stats.weeklyPosts.filter(p =>
new Date(p.date).getTime() > sevenDaysAgo
);
await saveStats(stats);
// Update queue if it was queued content
if (content.status === 'posting') {
content.status = 'posted';
content.postedAt = new Date().toISOString();
content.tweetId = result.tweetId;
await fs.writeFile(CONFIG.QUEUE_FILE, JSON.stringify(queue, null, 2));
}
// Log to posted file
let posted = [];
try {
posted = JSON.parse(await fs.readFile(CONFIG.POSTED_FILE, 'utf8'));
} catch (e) {
// File doesn't exist
}
posted.push({
...content,
tweet,
tweetId: result.tweetId,
url: `https://x.com/DearBubbe/status/${result.tweetId}`,
postedAt: new Date().toISOString()
});
// Keep only last 1000 posts
if (posted.length > 1000) {
posted = posted.slice(-1000);
}
await fs.writeFile(CONFIG.POSTED_FILE, JSON.stringify(posted, null, 2));
console.log(`✅ Posted successfully! (${stats.postsToday}/${CONFIG.MAX_DAILY_POSTS} today)`);
console.log(`URL: https://x.com/DearBubbe/status/${result.tweetId}`);
return true;
} else {
console.error('❌ Failed to post:', result.error);
return false;
}
} catch (error) {
console.error('❌ Error in postNext:', error);
return false;
}
}
// Generate weekly report
async function generateWeeklyReport() {
try {
const stats = await loadStats();
const report = {
generatedAt: new Date().toISOString(),
period: {
start: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString(),
end: new Date().toISOString()
},
summary: {
totalPosts: stats.weeklyPosts ? stats.weeklyPosts.length : 0,
dailyAverage: stats.weeklyPosts ? Math.round(stats.weeklyPosts.length / 7) : 0,
allTimePosts: stats.totalPosts || 0
},
dailyBreakdown: {},
categoryBreakdown: {},
topPosts: [],
recommendations: []
};
if (stats.weeklyPosts && stats.weeklyPosts.length > 0) {
// Daily breakdown
for (const post of stats.weeklyPosts) {
const day = new Date(post.date).toLocaleDateString();
report.dailyBreakdown[day] = (report.dailyBreakdown[day] || 0) + 1;
}
// Category breakdown
for (const post of stats.weeklyPosts) {
const cat = post.category || 'general';
report.categoryBreakdown[cat] = (report.categoryBreakdown[cat] || 0) + 1;
}
// Top posts (most recent for now)
report.topPosts = stats.weeklyPosts.slice(-10).reverse();
}
// Recommendations
if (report.summary.dailyAverage < CONFIG.MAX_DAILY_POSTS) {
report.recommendations.push(`Increase posting frequency - using only ${report.summary.dailyAverage}/${CONFIG.MAX_DAILY_POSTS} daily allowance`);
}
if (Object.keys(report.categoryBreakdown).length < 5) {
report.recommendations.push('Diversify content categories for better engagement');
}
// Save report
await fs.writeFile(CONFIG.WEEKLY_REPORT_FILE, JSON.stringify(report, null, 2));
console.log('\n📊 WEEKLY REPORT FOR STEVE');
console.log('=' .repeat(50));
console.log(`Period: ${report.period.start.split('T')[0]} to ${report.period.end.split('T')[0]}`);
console.log(`Total Posts: ${report.summary.totalPosts}`);
console.log(`Daily Average: ${report.summary.dailyAverage}/${CONFIG.MAX_DAILY_POSTS}`);
console.log(`All-Time Posts: ${report.summary.allTimePosts}`);
console.log('\nDaily Breakdown:');
for (const [day, count] of Object.entries(report.dailyBreakdown)) {
console.log(` ${day}: ${count} posts`);
}
console.log('\nCategory Breakdown:');
for (const [cat, count] of Object.entries(report.categoryBreakdown)) {
console.log(` ${cat}: ${count} posts`);
}
if (report.recommendations.length > 0) {
console.log('\nRecommendations:');
report.recommendations.forEach(rec => console.log(` • ${rec}`));
}
console.log('=' .repeat(50));
return report;
} catch (error) {
console.error('❌ Error generating report:', error);
return null;
}
}
// Main scheduler loop
async function runScheduler() {
if (postingState.isRunning) {
console.log('⚠️ Scheduler already running');
return;
}
postingState.isRunning = true;
console.log('🚀 Twitter Scheduler Started');
console.log(`Max posts per day: ${CONFIG.MAX_DAILY_POSTS}`);
console.log(`Min interval: ${CONFIG.MIN_INTERVAL_MINUTES} minutes`);
// Main loop
const scheduleNext = async () => {
if (!postingState.isRunning) return;
const stats = await loadStats();
const schedule = calculatePostingSchedule(stats.postsToday);
if (schedule && canPostNow(stats)) {
await postNext();
}
// Check if it's time for weekly report (Sunday at midnight)
const now = new Date();
if (now.getDay() === 0 && now.getHours() === 0 && now.getMinutes() === 0) {
await generateWeeklyReport();
}
// Schedule next check
const nextCheck = schedule ? schedule * 60 * 1000 : CONFIG.QUEUE_CHECK_INTERVAL;
setTimeout(scheduleNext, nextCheck);
};
// Start the loop
scheduleNext();
}
// Stop scheduler
function stopScheduler() {
postingState.isRunning = false;
console.log('🛑 Scheduler stopped');
}
// Export functions
module.exports = {
runScheduler,
stopScheduler,
postNext,
generateWeeklyReport,
loadStats,
generateContent
};
// Run if called directly
if (require.main === module) {
const command = process.argv[2];
if (command === 'start') {
runScheduler();
} else if (command === 'post') {
postNext().then(() => process.exit(0));
} else if (command === 'report') {
generateWeeklyReport().then(() => process.exit(0));
} else if (command === 'stats') {
loadStats().then(stats => {
console.log('📊 Current Stats:');
console.log(JSON.stringify(stats, null, 2));
process.exit(0);
});
} else {
console.log('Usage: node twitter-scheduler.js [start|post|report|stats]');
process.exit(1);
}
}