← 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
};