← back to Dear Bubbe Nextjs
archived/social-posters/social-autoposter.js
513 lines
#!/usr/bin/env node
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const { format } = require('date-fns');
// Configuration
const CONFIG = {
BUBBE_API_URL: 'http://localhost:3011/api/chat',
TWITTER_API_URL: 'https://api.twitter.com/2/tweets',
BLUESKY_API_URL: 'https://bsky.social/xrpc',
LOG_DIR: path.join(__dirname, 'logs'),
LOG_FILE: 'social-posts-detailed.log',
MAX_TWEET_LENGTH: 280,
MAX_BLUESKY_LENGTH: 300,
// Social media credentials (to be added to .env.local)
TWITTER_BEARER_TOKEN: process.env.TWITTER_BEARER_TOKEN,
BLUESKY_IDENTIFIER: process.env.BLUESKY_IDENTIFIER,
BLUESKY_PASSWORD: process.env.BLUESKY_PASSWORD
};
// Ensure log directory exists
if (!fs.existsSync(CONFIG.LOG_DIR)) {
fs.mkdirSync(CONFIG.LOG_DIR, { recursive: true });
}
// Logger class for detailed logging
class SocialLogger {
constructor() {
this.logPath = path.join(CONFIG.LOG_DIR, CONFIG.LOG_FILE);
}
log(level, message, data = {}) {
const timestamp = format(new Date(), 'yyyy-MM-dd HH:mm:ss.SSS');
const logEntry = {
timestamp,
level,
message,
...data
};
const logLine = JSON.stringify(logEntry);
// Write to file
fs.appendFileSync(this.logPath, logLine + '\n');
// Also log to console
console.log(`[${timestamp}] [${level}] ${message}`, data);
}
logPostAttempt(platform, userMessage, bubbeResponse, status, details = {}) {
const logData = {
platform,
userMessageSnippet: userMessage.substring(0, 50) + (userMessage.length > 50 ? '...' : ''),
bubbeResponseSnippet: bubbeResponse.substring(0, 100) + (bubbeResponse.length > 100 ? '...' : ''),
fullUserMessage: userMessage,
fullBubbeResponse: bubbeResponse,
status,
postLength: bubbeResponse.length,
...details
};
this.log(status === 'success' ? 'INFO' : 'ERROR', `Social post attempt to ${platform}`, logData);
}
}
// Initialize logger
const logger = new SocialLogger();
// Bluesky authentication class
class BlueskyAuth {
constructor() {
this.session = null;
}
async authenticate() {
try {
logger.log('INFO', 'Attempting Bluesky authentication');
const response = await axios.post(
`${CONFIG.BLUESKY_API_URL}/com.atproto.server.createSession`,
{
identifier: CONFIG.BLUESKY_IDENTIFIER,
password: CONFIG.BLUESKY_PASSWORD
}
);
this.session = response.data;
logger.log('INFO', 'Bluesky authentication successful', {
handle: this.session.handle,
did: this.session.did
});
return this.session;
} catch (error) {
logger.log('ERROR', 'Bluesky authentication failed', {
error: error.message,
response: error.response?.data
});
throw error;
}
}
async refreshSession() {
if (!this.session?.refreshJwt) {
return await this.authenticate();
}
try {
const response = await axios.post(
`${CONFIG.BLUESKY_API_URL}/com.atproto.server.refreshSession`,
{},
{
headers: {
'Authorization': `Bearer ${this.session.refreshJwt}`
}
}
);
this.session = response.data;
logger.log('INFO', 'Bluesky session refreshed');
return this.session;
} catch (error) {
logger.log('WARN', 'Session refresh failed, re-authenticating', {
error: error.message
});
return await this.authenticate();
}
}
}
// Social Media Poster class
class SocialMediaPoster {
constructor() {
this.blueskyAuth = new BlueskyAuth();
}
// Get Bubbe's response for a user message
async getBubbeResponse(userMessage) {
try {
logger.log('INFO', 'Getting Bubbe response', { userMessage });
const response = await axios.post(CONFIG.BUBBE_API_URL, {
message: userMessage,
mode: 'bubbe',
userId: 'social-autoposter'
});
const bubbeResponse = response.data.response || response.data;
logger.log('INFO', 'Bubbe response received', {
responseLength: bubbeResponse.length,
responseSnippet: bubbeResponse.substring(0, 100)
});
return bubbeResponse;
} catch (error) {
logger.log('ERROR', 'Failed to get Bubbe response', {
error: error.message,
response: error.response?.data
});
throw error;
}
}
// Format the social media post
formatPost(userMessage, bubbeResponse, platform) {
const maxLength = platform === 'twitter' ? CONFIG.MAX_TWEET_LENGTH : CONFIG.MAX_BLUESKY_LENGTH;
// Format: "Q: [truncated question]\nA: [truncated answer]"
let formattedPost = `Q: ${userMessage}\nA: ${bubbeResponse}`;
// If too long, truncate the response
if (formattedPost.length > maxLength) {
const availableLength = maxLength - userMessage.length - 7; // "Q: \nA: "
const truncatedResponse = bubbeResponse.substring(0, availableLength - 3) + '...';
formattedPost = `Q: ${userMessage}\nA: ${truncatedResponse}`;
}
logger.log('INFO', `Formatted post for ${platform}`, {
originalLength: userMessage.length + bubbeResponse.length,
formattedLength: formattedPost.length,
maxLength
});
return formattedPost;
}
// Post to Twitter/X
async postToTwitter(content) {
const startTime = Date.now();
try {
logger.log('INFO', 'Attempting to post to Twitter', {
contentLength: content.length
});
if (!CONFIG.TWITTER_BEARER_TOKEN) {
throw new Error('Twitter Bearer Token not configured');
}
const response = await axios.post(
CONFIG.TWITTER_API_URL,
{
text: content
},
{
headers: {
'Authorization': `Bearer ${CONFIG.TWITTER_BEARER_TOKEN}`,
'Content-Type': 'application/json'
}
}
);
const postId = response.data.data.id;
const elapsed = Date.now() - startTime;
logger.log('INFO', 'Successfully posted to Twitter', {
postId,
responseTime: `${elapsed}ms`,
url: `https://twitter.com/i/web/status/${postId}`
});
return {
success: true,
postId,
platform: 'twitter',
url: `https://twitter.com/i/web/status/${postId}`
};
} catch (error) {
const elapsed = Date.now() - startTime;
logger.log('ERROR', 'Failed to post to Twitter', {
error: error.message,
response: error.response?.data,
statusCode: error.response?.status,
responseTime: `${elapsed}ms`
});
return {
success: false,
platform: 'twitter',
error: error.message
};
}
}
// Post to Bluesky
async postToBluesky(content) {
const startTime = Date.now();
try {
logger.log('INFO', 'Attempting to post to Bluesky', {
contentLength: content.length
});
if (!CONFIG.BLUESKY_IDENTIFIER || !CONFIG.BLUESKY_PASSWORD) {
throw new Error('Bluesky credentials not configured');
}
// Ensure we have a valid session
if (!this.blueskyAuth.session) {
await this.blueskyAuth.authenticate();
}
const response = await axios.post(
`${CONFIG.BLUESKY_API_URL}/com.atproto.repo.createRecord`,
{
repo: this.blueskyAuth.session.did,
collection: 'app.bsky.feed.post',
record: {
text: content,
createdAt: new Date().toISOString()
}
},
{
headers: {
'Authorization': `Bearer ${this.blueskyAuth.session.accessJwt}`,
'Content-Type': 'application/json'
}
}
);
const postUri = response.data.uri;
const postCid = response.data.cid;
const elapsed = Date.now() - startTime;
// Extract the post ID from the URI
const postId = postUri.split('/').pop();
const postUrl = `https://bsky.app/profile/${this.blueskyAuth.session.handle}/post/${postId}`;
logger.log('INFO', 'Successfully posted to Bluesky', {
postUri,
postCid,
postId,
responseTime: `${elapsed}ms`,
url: postUrl
});
return {
success: true,
postId,
postUri,
platform: 'bluesky',
url: postUrl
};
} catch (error) {
const elapsed = Date.now() - startTime;
// If authentication error, try to re-authenticate once
if (error.response?.status === 401 && !error.retried) {
logger.log('WARN', 'Authentication expired, retrying with new session');
await this.blueskyAuth.authenticate();
error.retried = true;
return await this.postToBluesky(content);
}
logger.log('ERROR', 'Failed to post to Bluesky', {
error: error.message,
response: error.response?.data,
statusCode: error.response?.status,
responseTime: `${elapsed}ms`
});
return {
success: false,
platform: 'bluesky',
error: error.message
};
}
}
// Main function to post to all platforms
async postToAllPlatforms(userMessage) {
const sessionStartTime = Date.now();
logger.log('INFO', '=== STARTING SOCIAL MEDIA POST SESSION ===', {
userMessage,
timestamp: new Date().toISOString()
});
try {
// Get Bubbe's response
const bubbeResponse = await this.getBubbeResponse(userMessage);
// Format posts for each platform
const twitterContent = this.formatPost(userMessage, bubbeResponse, 'twitter');
const blueskyContent = this.formatPost(userMessage, bubbeResponse, 'bluesky');
// Post to both platforms
const results = await Promise.allSettled([
this.postToTwitter(twitterContent),
this.postToBluesky(blueskyContent)
]);
// Process results
const twitterResult = results[0].status === 'fulfilled' ? results[0].value : {
success: false,
platform: 'twitter',
error: results[0].reason?.message
};
const blueskyResult = results[1].status === 'fulfilled' ? results[1].value : {
success: false,
platform: 'bluesky',
error: results[1].reason?.message
};
// Log final results for each platform
logger.logPostAttempt(
'twitter',
userMessage,
bubbeResponse,
twitterResult.success ? 'success' : 'failed',
twitterResult
);
logger.logPostAttempt(
'bluesky',
userMessage,
bubbeResponse,
blueskyResult.success ? 'success' : 'failed',
blueskyResult
);
const sessionDuration = Date.now() - sessionStartTime;
// Summary log
logger.log('INFO', '=== SOCIAL MEDIA POST SESSION COMPLETE ===', {
duration: `${sessionDuration}ms`,
twitterSuccess: twitterResult.success,
blueskySuccess: blueskyResult.success,
twitterUrl: twitterResult.url,
blueskyUrl: blueskyResult.url,
timestamp: new Date().toISOString()
});
return {
twitter: twitterResult,
bluesky: blueskyResult,
sessionDuration
};
} catch (error) {
const sessionDuration = Date.now() - sessionStartTime;
logger.log('ERROR', 'Social media post session failed', {
error: error.message,
duration: `${sessionDuration}ms`,
timestamp: new Date().toISOString()
});
throw error;
}
}
}
// Test function
async function testSocialPosting() {
const poster = new SocialMediaPoster();
// Test messages that would trigger Bubbe's personality
const testMessages = [
"What's the weather like today?",
"Should I invest in Bitcoin?",
"My boyfriend hasn't called in 3 days"
];
// Use a random test message
const testMessage = testMessages[Math.floor(Math.random() * testMessages.length)];
logger.log('INFO', '🚀 STARTING TEST POST', {
testMessage,
timestamp: new Date().toISOString()
});
try {
const results = await poster.postToAllPlatforms(testMessage);
logger.log('INFO', '✅ TEST COMPLETE', {
results,
timestamp: new Date().toISOString()
});
// Print summary to console
console.log('\n========== TEST RESULTS ==========');
console.log(`Test Message: "${testMessage}"`);
console.log('\nTwitter/X:');
console.log(` Status: ${results.twitter.success ? '✅ SUCCESS' : '❌ FAILED'}`);
if (results.twitter.success) {
console.log(` URL: ${results.twitter.url}`);
} else {
console.log(` Error: ${results.twitter.error}`);
}
console.log('\nBluesky:');
console.log(` Status: ${results.bluesky.success ? '✅ SUCCESS' : '❌ FAILED'}`);
if (results.bluesky.success) {
console.log(` URL: ${results.bluesky.url}`);
} else {
console.log(` Error: ${results.bluesky.error}`);
}
console.log(`\nTotal Duration: ${results.sessionDuration}ms`);
console.log(`Log File: ${path.join(CONFIG.LOG_DIR, CONFIG.LOG_FILE)}`);
console.log('==================================\n');
} catch (error) {
logger.log('ERROR', '❌ TEST FAILED', {
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
console.error('\n❌ TEST FAILED:', error.message);
}
}
// Main execution
if (require.main === module) {
// Check for command line arguments
const args = process.argv.slice(2);
if (args.includes('--test')) {
testSocialPosting();
} else if (args.length > 0) {
// Use the provided message
const userMessage = args.join(' ');
const poster = new SocialMediaPoster();
poster.postToAllPlatforms(userMessage)
.then(results => {
console.log('Posted successfully:', results);
process.exit(0);
})
.catch(error => {
console.error('Failed to post:', error);
process.exit(1);
});
} else {
console.log('Usage:');
console.log(' node social-autoposter.js --test # Run a test post');
console.log(' node social-autoposter.js "Your message here" # Post a specific message');
}
}
// Export for use as module
module.exports = {
SocialMediaPoster,
SocialLogger
};