← back to Dear Bubbe Nextjs
archived/social-posters/bluesky-smart-scheduler.js
251 lines
#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const axios = require('axios');
const { postToBluesky } = require('./bluesky-poster');
// Configuration
const CONFIG = {
MAX_DAILY_POSTS: 3, // 3 posts per day as requested
MIN_INTERVAL_SECONDS: 14400, // 4 hours minimum between posts
STATS_FILE: '/root/Projects/dear-bubbe-admin/logs/bluesky-stats.json',
LAST_POST_FILE: '/root/Projects/dear-bubbe-admin/logs/last-bluesky-post.json'
};
// Content templates
const CONTENT = [
"My boyfriend only texts once a day",
"Should I quit my job?",
"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"
];
// 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();
// Peak hours for Bluesky (different audience than Twitter)
const peakHours = {
8: 5, // 8am
9: 4, // 9am
12: 4, // noon
17: 4, // 5pm
18: 5, // 6pm
19: 5, // 7pm
20: 4, // 8pm
21: 4 // 9pm
};
// Get target posts for this hour
let targetPostsThisHour = peakHours[hour] || 3;
// Calculate interval
const minutesLeftInHour = 60 - now.getMinutes();
let intervalMinutes = Math.floor(minutesLeftInHour / targetPostsThisHour);
// Ensure we hit daily target
const minutesLeftToday = hoursLeft * 60;
const neededInterval = Math.floor(minutesLeftToday / postsRemaining);
intervalMinutes = Math.min(intervalMinutes, neededInterval);
// Add randomness
const variance = Math.floor(intervalMinutes * 0.2);
intervalMinutes += Math.floor(Math.random() * (variance * 2 + 1) - variance);
// Enforce minimum
intervalMinutes = Math.max(3, intervalMinutes);
return intervalMinutes * 60;
}
// 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: 'bluesky-bot-' + Date.now()
});
return response.data.response;
} catch (error) {
console.error('Failed to get Bubbe response:', error.message);
return null;
}
}
// Format for Bluesky - clean, complete sentences
function formatForBluesky(response) {
// Clean response
let clean = response
.replace(/\s+/g, ' ')
.trim();
// Get complete sentences
const sentences = clean.match(/[^.!?]+[.!?]+/g) || [clean];
const maxLength = 250; // Leave room for hashtags
let post = '';
for (const sentence of sentences) {
const trimmed = sentence.trim();
if (post.length + trimmed.length + 1 <= maxLength) {
post += (post ? ' ' : '') + trimmed;
} else {
break;
}
}
// Ensure it ends with punctuation
if (post && !post.match(/[.!?]$/)) {
post += '!';
}
return `${post} #BubbeAI #Bubbe #JewishGrandma Visit Bubbe.AI`;
}
// Check if can post
async function canPost() {
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;
}
if (stats.count >= CONFIG.MAX_DAILY_POSTS) {
console.log(`Daily limit reached: ${stats.count}/${CONFIG.MAX_DAILY_POSTS}`);
return false;
}
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
async function postOne() {
if (!await canPost()) {
return false;
}
const topic = CONTENT[Math.floor(Math.random() * CONTENT.length)];
console.log('📝 Topic:', topic);
const response = await getBubbeResponse(topic);
if (!response) {
console.log('No response from Bubbe');
return false;
}
const post = formatForBluesky(response);
console.log('🦋 Post:', post.substring(0, 100) + '...');
const result = await postToBluesky(post);
if (result.success) {
const stats = await updateStats();
console.log(`✅ Posted to Bluesky! (${stats.count}/${CONFIG.MAX_DAILY_POSTS} today)`);
console.log(`🔗 ${result.url}`);
return true;
} else {
console.log('❌ Failed:', result.error);
return false;
}
}
// Run scheduler
async function runScheduler() {
console.log('🦋 Bluesky Smart Scheduler Started');
console.log(`📊 Goal: ${CONFIG.MAX_DAILY_POSTS} posts per day`);
console.log(`⏱️ Min interval: ${CONFIG.MIN_INTERVAL_SECONDS}s`);
while (true) {
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;
await fs.writeFile(CONFIG.STATS_FILE, JSON.stringify(stats, null, 2));
}
const now = new Date();
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999);
const hoursLeft = (endOfDay - now) / (1000 * 60 * 60);
if (await canPost()) {
await postOne();
}
const nextInterval = calculateNextInterval(stats.count || 0, hoursLeft);
if (!nextInterval) {
console.log('✅ Daily goal reached!');
const msUntilMidnight = endOfDay - new Date() + 1000;
await new Promise(r => setTimeout(r, msUntilMidnight));
} else {
console.log(`⏱️ Next post in ${Math.round(nextInterval/60)} minutes`);
await new Promise(r => setTimeout(r, nextInterval * 1000));
}
} catch (error) {
console.error('❌ Error:', error);
await new Promise(r => setTimeout(r, 5 * 60 * 1000));
}
}
}
if (require.main === module) {
runScheduler().catch(console.error);
}