← back to Dear Bubbe Nextjs
archived/social-posters/bluesky-autoposter.js
278 lines
const puppeteer = require('puppeteer');
const fs = require('fs').promises;
const path = require('path');
// Configuration
const BLUESKY_USERNAME = 'Dear Bubbe';
const BLUESKY_PASSWORD = '*Blueaccess911*';
const BLUESKY_HANDLE = 'dearbubbe.bsky.social';
const COOKIES_PATH = path.join(__dirname, 'bluesky-cookies.json');
const POSTED_IDS_PATH = path.join(__dirname, 'posted-bluesky.json');
// Bubbe intro phrases for variety
const BUBBE_INTROS = [
"Bubbe tells it like it is",
"Bubbe's got no filter",
"Bubbe says what you're thinking",
"Bubbe drops truth bombs",
"Bubbe keeps it real",
"Bubbe's wisdom hits different",
"Bubbe doesn't sugarcoat",
"Bubbe serves reality checks",
"Bubbe's brutal honesty",
"Bubbe speaks facts",
"Bubbe's savage advice",
"Bubbe pulls no punches"
];
// Name anonymization function (same as Twitter)
function anonymizeText(text) {
const nameReplacements = {
// Male names
'john': 'David', 'mike': 'Michael', 'steve': 'Robert', 'james': 'William',
'david': 'John', 'robert': 'James', 'michael': 'Thomas', 'william': 'Charles',
'richard': 'Joseph', 'joseph': 'Daniel', 'thomas': 'Paul', 'charles': 'Mark',
// Female names
'mary': 'Sarah', 'jennifer': 'Jessica', 'linda': 'Emma', 'patricia': 'Emily',
'elizabeth': 'Anna', 'susan': 'Rachel', 'jessica': 'Rebecca', 'sarah': 'Hannah',
'karen': 'Michelle', 'nancy': 'Laura', 'betty': 'Amanda', 'dorothy': 'Nicole',
// Spouse/partner references
'my husband': 'my partner', 'my wife': 'my partner',
'my boyfriend': 'my partner', 'my girlfriend': 'my partner'
};
let anonymized = text;
for (const [original, replacement] of Object.entries(nameReplacements)) {
const regex = new RegExp(`\\b${original}\\b`, 'gi');
anonymized = anonymized.replace(regex, replacement);
}
// Remove sensitive info
anonymized = anonymized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]');
anonymized = anonymized.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, '[phone]');
anonymized = anonymized.replace(/\d+\s+[A-Za-z]+\s+(Street|St|Avenue|Ave|Road|Rd)/gi, '[address]');
return anonymized;
}
// Get random intro
function getRandomIntro() {
return BUBBE_INTROS[Math.floor(Math.random() * BUBBE_INTROS.length)];
}
// Format post content (Bluesky has 300 char limit)
function formatPost(userMessage, bubbeResponse) {
const intro = getRandomIntro();
const anonymizedUser = anonymizeText(userMessage).substring(0, 80);
const anonymizedResponse = anonymizeText(bubbeResponse).substring(0, 120);
const post = `${intro}:\n\nQ: "${anonymizedUser}..."\n\nBubbe: "${anonymizedResponse}..."\n\n#BubbeAI #Bubbe #JewishGrandma\n\nVisit Bubbe.AI`;
// Ensure post is under 300 characters
if (post.length > 300) {
return `${intro}: "${anonymizedResponse.substring(0, 100)}..." #BubbeAI\n\nVisit Bubbe.AI`.substring(0, 300);
}
return post;
}
// Save cookies for future sessions
async function saveCookies(page) {
const cookies = await page.cookies();
await fs.writeFile(COOKIES_PATH, JSON.stringify(cookies, null, 2));
}
// Load cookies from previous session
async function loadCookies(page) {
try {
const cookiesString = await fs.readFile(COOKIES_PATH, 'utf8');
const cookies = JSON.parse(cookiesString);
await page.setCookie(...cookies);
return true;
} catch (error) {
return false;
}
}
// Check if already posted
async function isAlreadyPosted(messageId) {
try {
const postedIds = JSON.parse(await fs.readFile(POSTED_IDS_PATH, 'utf8'));
return postedIds.includes(messageId);
} catch (error) {
return false;
}
}
// Mark as posted
async function markAsPosted(messageId) {
try {
let postedIds = [];
try {
postedIds = JSON.parse(await fs.readFile(POSTED_IDS_PATH, 'utf8'));
} catch (error) {
// File doesn't exist yet
}
if (!postedIds.includes(messageId)) {
postedIds.push(messageId);
if (postedIds.length > 1000) {
postedIds = postedIds.slice(-1000);
}
await fs.writeFile(POSTED_IDS_PATH, JSON.stringify(postedIds, null, 2));
}
} catch (error) {
console.error('Error marking as posted:', error);
}
}
// Main posting function
async function postToBluesky(userMessage, bubbeResponse, messageId = null) {
// Skip if no real content
if (!userMessage || !bubbeResponse ||
userMessage.length < 10 || bubbeResponse.length < 10) {
console.log('Skipping Bluesky - insufficient content');
return false;
}
// Check if already posted
if (messageId && await isAlreadyPosted(messageId)) {
console.log('Already posted to Bluesky:', messageId);
return false;
}
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
// Try to load cookies
const cookiesLoaded = await loadCookies(page);
// Navigate to Bluesky
await page.goto('https://bsky.app', { waitUntil: 'networkidle2' });
// Check if we need to login
const needsLogin = await page.evaluate(() => {
return window.location.pathname === '/';
});
if (needsLogin && !cookiesLoaded) {
console.log('Logging in to Bluesky...');
// Click sign in button
await page.waitForSelector('button:has-text("Sign in")', { timeout: 10000 });
await page.click('button:has-text("Sign in")');
// Enter username
await page.waitForSelector('input[placeholder*="Username"]', { timeout: 10000 });
await page.type('input[placeholder*="Username"]', BLUESKY_HANDLE);
// Enter password
await page.type('input[type="password"]', BLUESKY_PASSWORD);
// Click login
await page.click('button[type="submit"]');
// Wait for login to complete
await page.waitForNavigation({ waitUntil: 'networkidle2' });
// Save cookies for next time
await saveCookies(page);
console.log('Bluesky login successful, cookies saved');
}
// Format the post
const postContent = formatPost(userMessage, bubbeResponse);
console.log('Posting to Bluesky:', postContent.substring(0, 100) + '...');
// Click compose button
await page.waitForSelector('[aria-label="Compose new post"]', { timeout: 10000 });
await page.click('[aria-label="Compose new post"]');
// Wait for compose modal
await page.waitForSelector('textarea[placeholder*="What\'s up"]', { timeout: 10000 });
// Type the post
await page.type('textarea[placeholder*="What\'s up"]', postContent);
// Wait a moment
await page.waitForTimeout(1000);
// Click post button
await page.click('button:has-text("Post")');
// Wait for post to complete
await page.waitForTimeout(3000);
// Mark as posted
if (messageId) {
await markAsPosted(messageId);
}
console.log('Bluesky post successful!');
return true;
} catch (error) {
console.error('Error posting to Bluesky:', error);
return false;
} finally {
await browser.close();
}
}
// Auto-post from chat logs (shared with Twitter)
async function autoPostFromChats() {
try {
const chatLogPath = '/root/Projects/dear-bubbe-nextjs/logs/chat-history.json';
const chats = JSON.parse(await fs.readFile(chatLogPath, 'utf8'));
// Get chats from last 24 hours
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
const recentChats = chats.filter(chat =>
chat.timestamp && new Date(chat.timestamp).getTime() > oneDayAgo
);
// Sort by engagement
const interestingChats = recentChats
.filter(chat => chat.userMessage && chat.bubbeResponse)
.sort((a, b) => b.bubbeResponse.length - a.bubbeResponse.length)
.slice(0, 5);
// Post one random interesting chat
if (interestingChats.length > 0) {
const chat = interestingChats[Math.floor(Math.random() * interestingChats.length)];
await postToBluesky(chat.userMessage, chat.bubbeResponse, 'bsky-' + chat.id);
}
} catch (error) {
console.error('Error in Bluesky auto-post:', error);
}
}
// Export functions
module.exports = {
postToBluesky,
autoPostFromChats,
anonymizeText,
formatPost
};
// If run directly, test it
if (require.main === module) {
const testUser = "My wife Karen is always complaining about money!";
const testBubbe = "Money? She should complain! You probably spend it all on nonsense. Get a better job or stop buying schmatte!";
postToBluesky(testUser, testBubbe, 'bsky-test-' + Date.now())
.then(result => {
console.log('Bluesky test post result:', result);
process.exit(result ? 0 : 1);
});
}