Guest User

tweet-reminder

a guest
Nov 13th, 2024
337
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 12.91 KB | Source Code | 0 0
  1. const fs = require('fs').promises;
  2. const schedule = require('node-schedule');
  3. const { exec } = require('child_process');
  4. const util = require('util');
  5. const execPromise = util.promisify(exec);
  6.  
  7. class TweetReminder {
  8.     constructor(archivePath, options = {}) {
  9.         this.archivePath = archivePath;
  10.         this.debugLog = options.debugLog || (() => {});
  11.         this.options = {
  12.             useMacNotifications: options.useMacNotifications || false
  13.         };
  14.         this.tweets = [];
  15.     }
  16.  
  17.     async initialize() {
  18.         try {
  19.             this.tweets = await this.loadTweets();
  20.             this.debugLog(`Loaded ${this.tweets.length} tweets`);
  21.             return true;
  22.         } catch (error) {
  23.             console.error('Failed to initialize:', error);
  24.             return false;
  25.         }
  26.     }
  27.  
  28.     async loadTweets() {
  29.         try {
  30.             let data = await fs.readFile(this.archivePath, 'utf8');
  31.            
  32.             if (data.startsWith('window.YTD.tweets.part0 = ')) {
  33.                 data = data.replace('window.YTD.tweets.part0 = ', '');
  34.             }
  35.  
  36.             const parsedData = JSON.parse(data);
  37.             const tweets = parsedData.map(item => item.tweet);
  38.  
  39.             const originalTweets = tweets.filter(tweet => {
  40.                 return !tweet.retweeted &&
  41.                        !tweet.full_text.startsWith('RT ') &&
  42.                        !(tweet.in_reply_to_user_id || tweet.in_reply_to_status_id);
  43.             });
  44.  
  45.             this.debugLog(`Found ${originalTweets.length} original tweets`);
  46.             return originalTweets;
  47.         } catch (error) {
  48.             console.error('Error loading tweets:', error);
  49.             return [];
  50.         }
  51.     }
  52.  
  53.     selectInterestingTweet(seenIds = new Set()) {
  54.         if (!this.tweets.length) {
  55.             return null;
  56.         }
  57.  
  58.         // Define what we consider "interesting" engagement
  59.         const MIN_ENGAGEMENT = 5;  // Total of likes + retweets
  60.         const VIRAL_THRESHOLD = 1000;  // What we consider "viral"
  61.  
  62.         const availableTweets = this.tweets.filter(t => !seenIds.has(t.id_str));
  63.        
  64.         const weightedTweets = availableTweets.map(tweet => {
  65.             const likes = parseInt(tweet.favorite_count || 0);
  66.             const retweets = parseInt(tweet.retweet_count || 0);
  67.             const totalEngagement = likes + retweets;
  68.  
  69.             // Start with a base random component
  70.             let weight = Math.random();  // Full random base weight
  71.  
  72.             // Only add engagement weight if it meets minimum threshold
  73.             if (totalEngagement >= MIN_ENGAGEMENT) {
  74.                 // Cap engagement at viral threshold
  75.                 const cappedEngagement = Math.min(totalEngagement, VIRAL_THRESHOLD);
  76.                
  77.                 // For tweets below viral threshold, use logarithmic scaling
  78.                 // For viral tweets, they all get the same bonus
  79.                 const engagementWeight = Math.log10(cappedEngagement) / Math.log10(VIRAL_THRESHOLD);
  80.                 weight += engagementWeight * 0.5; // Engagement is worth up to 50% of weight
  81.  
  82.                 // Small boost for having both likes and retweets
  83.                 if (likes > 0 && retweets > 0) {
  84.                     weight *= 1.1;
  85.                 }
  86.             }
  87.  
  88.             // Tiny boost for longer, non-link tweets
  89.             const text = tweet.full_text;
  90.             if (text.length > 60 && !text.includes('http')) {
  91.                 weight *= 1.05;
  92.             }
  93.  
  94.             // Tiny boost for tweets that are part of a thread
  95.             if (tweet.in_reply_to_status_id_str) {
  96.                 weight *= 1.05;
  97.             }
  98.  
  99.             this.debugLog(`Tweet weight calculation:
  100.                 Text: ${text.substring(0, 50)}...
  101.                 Engagement: ${totalEngagement}
  102.                 Final weight: ${weight.toFixed(3)}
  103.             `);
  104.  
  105.             return { tweet, weight };
  106.         });
  107.  
  108.         // Use weighted random selection
  109.         const totalWeight = weightedTweets.reduce((sum, { weight }) => sum + weight, 0);
  110.         let randomValue = Math.random() * totalWeight;
  111.        
  112.         for (const { tweet, weight } of weightedTweets) {
  113.             randomValue -= weight;
  114.             if (randomValue <= 0) {
  115.                 return tweet;
  116.             }
  117.         }
  118.  
  119.         return weightedTweets[0]?.tweet || null;
  120.     }
  121.  
  122.     formatTweetReminder(tweet) {
  123.         const text = tweet.full_text;
  124.         const tweetDate = new Date(tweet.created_at).toLocaleDateString('en-US', {
  125.             year: 'numeric',
  126.             month: 'long',
  127.             day: 'numeric'
  128.         });
  129.  
  130.         // Create the tweet URL using the tweet ID
  131.         const tweetUrl = `https://twitter.com/i/status/${tweet.id_str}`;
  132.  
  133.         // Create the engagement string
  134.         const likes = parseInt(tweet.favorite_count || 0);
  135.         const retweets = parseInt(tweet.retweet_count || 0);
  136.         const engagement = `💖 ${likes} like${likes !== 1 ? 's' : ''}, 🔄 ${retweets} retweet${retweets !== 1 ? 's' : ''}`;
  137.  
  138.         return `
  139. 📅 On ${tweetDate}, you tweeted:
  140.  
  141. ===========================================
  142.  
  143. ${text}
  144.  
  145. 📊 Engagement: ${engagement}
  146.  
  147. ===========================================
  148.  
  149. 🔗 ${tweetUrl}`.trim();
  150.     }
  151.  
  152.     async sendMacNotification(tweets) {
  153.         try {
  154.             // Show notification for first tweet
  155.             const firstTweet = tweets[0];
  156.             const shortText = firstTweet.full_text.substring(0, 100) +
  157.                 (firstTweet.full_text.length > 100 ? '...' : '');
  158.            
  159.             const cleanShortText = shortText.replace(/[\\"`]/g, "'");
  160.            
  161.             // First, show the notification
  162.             await execPromise(`osascript -e 'display notification "${cleanShortText}" with title "Tweet Memory" subtitle "Click Show for digest" sound name "Glass"'`);
  163.  
  164.             // Create a temporary HTML file
  165.             const { writeFile, unlink } = require('fs').promises;
  166.             const os = require('os');
  167.             const path = require('path');
  168.             const tempPath = path.join(os.tmpdir(), 'tweet-digest.html');
  169.  
  170.             // Format tweets as HTML
  171.             const tweetsHtml = tweets.map((tweet, i) => {
  172.                 const tweetDate = new Date(tweet.created_at).toLocaleDateString('en-US', {
  173.                     year: 'numeric',
  174.                     month: 'long',
  175.                     day: 'numeric'
  176.                 });
  177.  
  178.                 const likes = parseInt(tweet.favorite_count || 0);
  179.                 const retweets = parseInt(tweet.retweet_count || 0);
  180.                 const stats = `${likes} ${likes === 1 ? 'like' : 'likes'}, ${retweets} ${retweets === 1 ? 'retweet' : 'retweets'}`;
  181.  
  182.                 return `
  183.                     <div class="tweet">
  184.                         <div class="header">Tweet ${i + 1}</div>
  185.                         <div class="date">${tweetDate}</div>
  186.                         <div class="text">${tweet.full_text}</div>
  187.                         <div class="stats">${stats}</div>
  188.                         <div class="url">
  189.                             <a href="https://twitter.com/i/status/${tweet.id_str}" target="_blank">
  190.                                 twitter.com/i/status/${tweet.id_str}
  191.                             </a>
  192.                         </div>
  193.                     </div>
  194.                     ${i < tweets.length - 1 ? '<hr/>' : ''}
  195.                 `.trim();
  196.             }).join('');
  197.  
  198.             const html = `
  199.                 <!DOCTYPE html>
  200.                 <html>
  201.                 <head>
  202.                     <meta charset="UTF-8">
  203.                     <style>
  204.                         body {
  205.                             font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  206.                             padding: 20px;
  207.                             max-width: 800px;
  208.                             margin: 0 auto;
  209.                             line-height: 1.4;
  210.                         }
  211.                         .tweet { margin-bottom: 20px; }
  212.                         .header { font-weight: bold; margin-bottom: 5px; }
  213.                         .date { color: #666; margin-bottom: 10px; }
  214.                         .text { margin: 15px 0; white-space: pre-wrap; }
  215.                         .stats { color: #666; margin-bottom: 5px; }
  216.                         .url a { color: #1da1f2; text-decoration: none; }
  217.                         .url a:hover { text-decoration: underline; }
  218.                         hr { border: none; border-top: 1px solid #eee; margin: 20px 0; }
  219.                     </style>
  220.                 </head>
  221.                 <body>${tweetsHtml}</body>
  222.                 </html>
  223.             `;
  224.  
  225.             await writeFile(tempPath, html);
  226.             await execPromise(`open "${tempPath}"`);
  227.            
  228.             // Clean up after a delay
  229.             setTimeout(() => {
  230.                 unlink(tempPath).catch(() => {});
  231.             }, 1000);
  232.            
  233.             return true;
  234.         } catch (error) {
  235.             console.error('Error showing tweets:', error);
  236.             return false;
  237.         }
  238.     }
  239.  
  240.     async sendDailyReminder() {
  241.         const digest = [];
  242.         const seenIds = new Set();
  243.        
  244.         // First collect all 5 tweets
  245.         for (let i = 0; i < 5; i++) {
  246.             const tweet = this.selectInterestingTweet(seenIds);
  247.             if (!tweet) break;
  248.             seenIds.add(tweet.id_str);
  249.             digest.push(tweet);
  250.         }
  251.  
  252.         // Print to console immediately
  253.         console.log('\n=== Tweet Memory Digest ===\n');
  254.         for (let i = 0; i < digest.length; i++) {
  255.             console.log(`[${i + 1} of 5]\n`);
  256.             console.log(this.formatTweetReminder(digest[i]));
  257.             if (i < digest.length - 1) {
  258.                 console.log('\n---\n');
  259.             }
  260.         }
  261.         console.log();  // Final newline
  262.  
  263.         // Then show notification if enabled
  264.         if (process.platform === 'darwin' && this.options?.useMacNotifications) {
  265.             await this.sendMacNotification(digest);
  266.         }
  267.     }
  268. }
  269.  
  270. async function main() {
  271.     const args = process.argv.slice(2);
  272.  
  273.     if (args.includes('--help') || args.includes('-h') || args.length === 0) {
  274.         console.log(`
  275. Usage: node tweet-reminder <archive-path> [options]
  276.  
  277. Description:
  278.     Surfaces interesting tweets from your Twitter archive as a daily digest of 5 tweets,
  279.     with an intelligent selection algorithm that balances engagement and randomness.
  280.     Shows tweets in terminal and optionally as system notifications.
  281.  
  282. Arguments:
  283.     archive-path    Path to your Twitter archive JSON file
  284.                    (usually named "tweets.js" in your archive)
  285.  
  286. Options:
  287.     --help, -h     Show this help message
  288.     --test         Run once and exit (don't schedule daily reminders)
  289.    --debug        Show debug logging information
  290.    --notify       Show macOS notification for the first tweet (includes clickable dialog)
  291.  
  292. Examples:
  293.    node tweet-reminder ./tweets.js
  294.    node tweet-reminder ./tweets.js --test
  295.    node tweet-reminder ./tweets.js --notify
  296.    node tweet-reminder ./tweets.js --test --notify
  297.    node tweet-reminder --help
  298.  
  299. Details:
  300.    - Runs daily at noon by default
  301.    - Shows 5 different tweets each time
  302.    - Prefers tweets with meaningful engagement
  303.    - Includes date, engagement stats, and original URL for each tweet
  304.    - With --notify, shows system notification for the first tweet
  305. `);
  306.        process.exit(0);
  307.    }
  308.  
  309.    const archivePath = args.find(arg => !arg.startsWith('-'));
  310.    if (!archivePath) {
  311.        console.error('Error: Archive path is required');
  312.        process.exit(1);
  313.    }
  314.  
  315.    const isDebug = args.includes('--debug');
  316.    const debugLog = (...args) => {
  317.        if (isDebug) {
  318.            console.log(...args);
  319.        }
  320.    };
  321.  
  322.    try {
  323.        await fs.access(archivePath);
  324.    } catch (error) {
  325.        console.error(`Error: Cannot access ${archivePath}`);
  326.        console.error('Please check if the file exists and you have permission to read it.');
  327.        process.exit(1);
  328.    }
  329.  
  330.    const reminder = new TweetReminder(archivePath, {
  331.        debugLog,
  332.        useMacNotifications: args.includes('--notify')
  333.    });
  334.  
  335.    const initialized = await reminder.initialize();
  336.    if (!initialized) {
  337.        console.error('Failed to initialize tweet reminder');
  338.        process.exit(1);
  339.    }
  340.  
  341.    if (args.includes('--test')) {
  342.        await reminder.sendDailyReminder();
  343.        process.exit(0);
  344.    }
  345.  
  346.    // Run immediately and schedule future runs
  347.    await reminder.sendDailyReminder();
  348.    schedule.scheduleJob('0 12 * * *', async () => {
  349.        debugLog('Running scheduled digest...');
  350.        await reminder.sendDailyReminder();
  351.    });
  352.  
  353.    console.log('\nTweet digest service started! Checking for tweets daily at 12:00 PM.');
  354.    console.log('Press Ctrl+C to exit');
  355. }
  356.  
  357. main().catch(err => {
  358.    console.error('Error in main:', err);
  359.    process.exit(1);
  360. });
Tags: twitter
Advertisement
Add Comment
Please, Sign In to add comment