← back to Dear Bubbe Nextjs
archived/social-posters/smart-twitter-scheduler.js
330 lines
#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const axios = require('axios');
const { postTweet } = require('./twitter-v2-official');
// Configuration
const CONFIG = {
MAX_DAILY_POSTS: 100,
MIN_INTERVAL_SECONDS: 180, // 3 minutes minimum
STATS_FILE: '/root/Projects/dear-bubbe-admin/logs/twitter-stats.json',
LAST_POST_FILE: '/root/Projects/dear-bubbe-admin/logs/last-twitter-post.json'
};
// Viral hashtags that work
const HASHTAGS = {
always: ['#BubbeAI', '#Bubbe'],
viral: ['#JewishTwitter', '#Dating', '#Relationships', '#MondayMotivation', '#Funny', '#Comedy', '#NoFilter', '#Truth', '#Savage', '#Life'],
day: {
0: '#SundayFunday',
1: '#MondayMotivation',
2: '#TuesdayThoughts',
3: '#WednesdayWisdom',
4: '#ThursdayThoughts',
5: '#FridayFeeling',
6: '#SaturdayVibes'
}
};
// Content templates
const CONTENT = [
"My boyfriend only texts once a day",
"Should I quit my job without another lined up?",
"My mother-in-law criticized my cooking",
"I spent $500 on shoes",
"Is 35 too old to start dating?",
"My husband forgot our anniversary",
"Should I text my ex?",
"I haven't been to temple in years",
"My kids never call me",
"My boss is micromanaging me",
"I'm thinking about getting married",
"My friend owes me money",
"Should I move back home?",
"I hate my job but it pays well",
"My partner doesn't help with chores",
"I'm 30 and still single",
"My parents don't approve of my boyfriend",
"Should I get a dog?",
"I lied to my best friend",
"My neighbor is too noisy"
];
// Calculate smart posting interval
function calculateNextInterval(postsToday, hoursLeft) {
const postsRemaining = CONFIG.MAX_DAILY_POSTS - postsToday;
if (postsRemaining <= 0) return null;
if (hoursLeft <= 0) return CONFIG.MIN_INTERVAL_SECONDS;
const now = new Date();
const hour = now.getHours();
const minute = now.getMinutes();
// Peak engagement hours (more frequent posting)
const peakHours = {
7: 5, // 7am - morning commute
8: 5, // 8am
9: 4, // 9am - work start
12: 4, // noon - lunch
13: 4, // 1pm
17: 4, // 5pm - end of work
18: 5, // 6pm - commute home
19: 5, // 7pm - dinner
20: 4, // 8pm - evening
21: 4 // 9pm - before bed
};
// Off-peak hours (less frequent)
const offPeakHours = {
0: 1, // midnight
1: 1, // 1am
2: 1, // 2am
3: 1, // 3am
4: 1, // 4am
5: 2, // 5am
6: 3, // 6am
10: 3, // 10am
11: 3, // 11am
14: 3, // 2pm
15: 3, // 3pm
16: 3, // 4pm
22: 2, // 10pm
23: 2 // 11pm
};
// Get target posts for this hour
let targetPostsThisHour = offPeakHours[hour] || 3;
if (peakHours[hour]) {
targetPostsThisHour = peakHours[hour];
}
// Calculate how many minutes between posts this hour
const minutesLeftInHour = 60 - minute;
let intervalMinutes = Math.floor(minutesLeftInHour / targetPostsThisHour);
// Ensure we hit our daily target
const minutesLeftToday = hoursLeft * 60;
const neededInterval = Math.floor(minutesLeftToday / postsRemaining);
// Use the smaller interval to ensure we use all posts
intervalMinutes = Math.min(intervalMinutes, neededInterval);
// Add some randomness (±20%)
const variance = Math.floor(intervalMinutes * 0.2);
intervalMinutes += Math.floor(Math.random() * (variance * 2 + 1) - variance);
// Enforce minimum interval
intervalMinutes = Math.max(3, intervalMinutes); // 3 minutes minimum
console.log(`📊 Scheduling: ${postsRemaining} posts left, ${hoursLeft.toFixed(1)}h remaining`);
console.log(`⏱️ Next post in ${intervalMinutes} minutes`);
return intervalMinutes * 60; // Return in seconds
}
// Get Bubbe response
async function getBubbeResponse(message) {
try {
const response = await axios.post('http://45.61.58.125:3011/api/chat', {
message: message,
mode: 'bubbe',
userId: 'twitter-bot-' + Date.now()
});
return response.data.response;
} catch (error) {
console.error('Failed to get Bubbe response:', error.message);
return null;
}
}
// Format tweet - clean, no ellipsis
function formatTweet(response) {
const { formatCleanTweet } = require('./twitter-clean-formatter');
return formatCleanTweet('', response);
}
// Check if can post
async function canPost() {
try {
// Check daily limit
const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
const today = new Date().toDateString();
if (stats.date !== today) {
stats.date = today;
stats.count = 0;
}
if (stats.count >= CONFIG.MAX_DAILY_POSTS) {
console.log(`Daily limit reached: ${stats.count}/${CONFIG.MAX_DAILY_POSTS}`);
return false;
}
// Check time since last post
const lastPost = JSON.parse(await fs.readFile(CONFIG.LAST_POST_FILE, 'utf8').catch(() => '{}'));
if (lastPost.time) {
const elapsed = (Date.now() - lastPost.time) / 1000;
if (elapsed < CONFIG.MIN_INTERVAL_SECONDS) {
console.log(`Rate limit: wait ${Math.ceil(CONFIG.MIN_INTERVAL_SECONDS - elapsed)}s`);
return false;
}
}
return true;
} catch (error) {
console.error('Error checking limits:', error);
return false;
}
}
// Update stats
async function updateStats() {
try {
const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
const today = new Date().toDateString();
if (stats.date !== today) {
stats.date = today;
stats.count = 0;
}
stats.count++;
stats.total = (stats.total || 0) + 1;
await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
await fs.writeFile(CONFIG.LAST_POST_FILE, JSON.stringify({ time: Date.now() }, null, 2));
return stats;
} catch (error) {
console.error('Error updating stats:', error);
}
}
// Post one tweet
async function postOne() {
if (!await canPost()) {
return false;
}
// Get random content
const topic = CONTENT[Math.floor(Math.random() * CONTENT.length)];
console.log('📝 Topic:', topic);
// Get Bubbe response
const response = await getBubbeResponse(topic);
if (!response) {
console.log('No response from Bubbe');
return false;
}
// Format and post
const tweet = formatTweet(response);
console.log('🐦 Tweet:', tweet.substring(0, 100) + '...');
console.log('📏 Length:', tweet.length);
const result = await postTweet(tweet);
if (result.success) {
const stats = await updateStats();
console.log(`✅ Posted! (${stats.count}/${CONFIG.MAX_DAILY_POSTS} today, ${stats.total} total)`);
console.log(`🔗 URL: https://x.com/DearBubbe/status/${result.tweetId}`);
return true;
} else {
console.log('❌ Failed:', result.error);
return false;
}
}
// Run smart scheduler
async function runSmartScheduler() {
console.log('🚀 Smart Twitter Scheduler Started');
console.log(`📊 Goal: ${CONFIG.MAX_DAILY_POSTS} posts per day`);
console.log(`⏱️ Min interval: ${CONFIG.MIN_INTERVAL_SECONDS}s (3 minutes)`);
console.log('');
while (true) {
try {
// Get current stats
const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
const today = new Date().toDateString();
if (stats.date !== today) {
stats.date = today;
stats.count = 0;
await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
}
// Calculate time remaining today
const now = new Date();
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const hoursLeft = (endOfDay - now) / (1000 * 60 * 60);
// Post if we can
if (await canPost()) {
await postOne();
}
// Calculate next interval
const nextInterval = calculateNextInterval(stats.count || 0, hoursLeft);
if (!nextInterval) {
console.log('✅ Daily goal reached! Waiting for tomorrow...');
// Wait until midnight
const msUntilMidnight = endOfDay - new Date() + 1000;
await new Promise(r => setTimeout(r, msUntilMidnight));
} else {
// Wait for next post
await new Promise(r => setTimeout(r, nextInterval * 1000));
}
} catch (error) {
console.error('❌ Scheduler error:', error);
// Wait 5 minutes on error
await new Promise(r => setTimeout(r, 5 * 60 * 1000));
}
}
}
// Get status
async function getStatus() {
const stats = JSON.parse(await fs.readFile(CONFIG.STATS_FILE, 'utf8').catch(() => '{}'));
const now = new Date();
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const hoursLeft = (endOfDay - now) / (1000 * 60 * 60);
const postsRemaining = CONFIG.MAX_DAILY_POSTS - (stats.count || 0);
console.log('\n📊 TWITTER SCHEDULER STATUS');
console.log('=' .repeat(40));
console.log(`Today: ${now.toDateString()}`);
console.log(`Posts today: ${stats.count || 0}/${CONFIG.MAX_DAILY_POSTS}`);
console.log(`Posts remaining: ${postsRemaining}`);
console.log(`Hours left today: ${hoursLeft.toFixed(1)}`);
if (postsRemaining > 0 && hoursLeft > 0) {
const avgInterval = (hoursLeft * 60) / postsRemaining;
console.log(`Average interval needed: ${avgInterval.toFixed(1)} minutes`);
if (avgInterval < 3) {
console.log('⚠️ WARNING: May not reach daily goal!');
} else {
console.log('✅ On track to reach daily goal');
}
}
console.log('=' .repeat(40));
}
// Start
if (require.main === module) {
const command = process.argv[2];
if (command === 'status') {
getStatus().then(() => process.exit(0));
} else {
runSmartScheduler().catch(console.error);
}
}