👤 Twitter/X Profile Posts Scraper

Scrapers scripts/twitter
v2.0.0 Updated: 2026-01-26 872 lines by @nichxbt

Scrapes posts from any Twitter/X profile with filtering,

How to Use

  1. Navigate to x.com and log in
  2. Open DevTools Console (F12 or Cmd+Option+I)
  3. Paste the script below and press Enter

Default Configuration

const CONFIG = {
  
  // ==========================================
  // 📊 SCRAPING SETTINGS
  // ==========================================
  
  // Number of tweets to scrape (max)
  // 💡 Set to a higher number for more tweets, but it takes longer
  targetCount: 300,
  
  // Maximum scroll attempts before giving up
  // 💡 Increase if the profile has lots of media (slower loading)
  maxScrollAttempts: 300,
  
  // Delay between scrolls (milliseconds)
  // 💡 Increase to 3000-30000 if tweets aren't loading properly
  scrollDelay: 2000,
  
  // ==========================================
  // 🔍 FILTERING OPTIONS
  // ==========================================
  // Use these to only keep tweets matching your criteria
  
  filters: {
    
    // ---- KEYWORD WHITELIST ----
    // Only include tweets containing AT LEAST ONE of these words
    // 💡 Leave empty [] to include all tweets
    // 💡 Example: ['crypto', 'bitcoin', 'eth'] - keeps tweets with any of these words
    // 💡 Case-insensitive (matches 'Bitcoin', 'BITCOIN', 'bitcoin')
    whitelist: [],
    
    // ---- KEYWORD BLACKLIST ----
    // Exclude tweets containing ANY of these words
    // 💡 Leave empty [] to not exclude anything
    // 💡 Example: ['giveaway', 'spam', 'ad'] - removes tweets with these words
    // 💡 Case-insensitive
    blacklist: [],
    
    // ---- DATE RANGE ----
    // Only include tweets from the last X days
    // 💡 Set to 0 to include all tweets (no date filter)
    // 💡 Example: 7 = last week, 30 = last month, 365 = last year
    daysBack: 0,
    
    // ---- MINIMUM ENGAGEMENT ----
    // Only include tweets with at least this many likes
    // 💡 Set to 0 to include all tweets regardless of engagement
    // 💡 Example: 10 = only tweets with 10+ likes
    minLikes: 0,
    
    // Only include tweets with at least this many retweets
    // 💡 Set to 0 to include all
    minRetweets: 0,
    
    // ---- CONTENT TYPE FILTERS ----
    // Set to true to EXCLUDE these types of content
    
    // Exclude retweets (posts this user reposted from others)
    // 💡 Set to true to only see original content
    excludeRetweets: false,
    
    // Exclude replies (tweets that are responses to others)
    // 💡 Set to true to only see top-level posts
    excludeReplies: false,
    
    // ---- MEDIA FILTERS ----
    // Filter by media content
    // 💡 Options: 'all' | 'with-media' | 'without-media'
    // 💡 'all' = include everything
    // 💡 'with-media' = only tweets with images/videos
    // 💡 'without-media' = only text-only tweets
    mediaFilter: 'all'
  },
  
  // ==========================================
  // 📤 EXPORT FORMAT SETTINGS
  // ==========================================
  // Choose which formats to export. Set to true/false for each.
  
  export: {
    
    // ---- JSON FORMAT ----
    // Full data with all fields, good for programming/analysis
    // 💡 Best for: Importing into other tools, APIs, databases
    json: true,
    
    // ---- CSV FORMAT ----
    // Spreadsheet-compatible format
    // 💡 Best for: Opening in Excel, Google Sheets, data analysis
    // 💡 Opens directly in spreadsheet apps
    csv: true,
    
    // ---- MARKDOWN FORMAT ----
    // Formatted text with headers and lists
    // 💡 Best for: Documentation, Notion, Obsidian, note-taking apps
    markdown: false,
    
    // ---- PLAIN TEXT FORMAT ----
    // Simple readable text, one tweet per block
    // 💡 Best for: Reading, sharing, simple archives
    text: false,
    
    // ---- HTML TABLE ----
    // Styled HTML table you can embed in websites
    // 💡 Best for: Reports, presentations, embedding in web pages
    html: false
  },
  
  // ==========================================
  // 📊 DISPLAY & ANALYTICS
  // ==========================================
  
  display: {
    
    // Show summary statistics (total engagement, averages, etc.)
    // 💡 Displays: total likes/retweets, averages, top hashtags
    showStats: true,
    
    // Show top performing tweets (sorted by engagement)
    // 💡 Set to 0 to disable, or 5-10 to show top posts
    showTopPosts: 5,
    
    // Show extracted hashtags from all tweets
    // 💡 Useful to see trending topics for this profile
    showHashtags: true,
    
    // Show all @mentions found in tweets
    // 💡 Useful to see who this profile interacts with
    showMentions: true,
    
    // Show all URLs/links shared
    // 💡 Useful to see what resources they share
    showLinks: false,
    
    // Pretty print tweets in console (formatted text view)
    // 💡 Makes it easy to read tweets directly in console
    prettyPrint: true,
    
    // Number of tweets to pretty print (set lower to reduce console spam)
    prettyPrintLimit: 10
  },
  
  // ==========================================
  // 🔧 GENERAL SETTINGS
  // ==========================================
  
  // Copy results to clipboard when complete
  // 💡 Copies the primary export format (JSON by default)
  copyToClipboard: true,
  
  // Show verbose progress messages
  // 💡 Set to false for cleaner output
  verbose: true
};

Full Script

Copy and paste this entire script into your browser DevTools console on x.com.

/**
 * ============================================================
 * 🐦 Twitter/X Profile Posts Scraper (Advanced)
 * ============================================================
 * 
 * @name        scrape-profile-posts.js
 * @description Scrapes posts from any Twitter/X profile with filtering,
 *              multiple export formats, and analytics
 * @author      nichxbt (https://x.com/nichxbt)
 * @version     2.0.0
 * @date        2026-01-26
 * @repository  https://github.com/nirholas/XActions
 * 
 * ============================================================
 * 📋 USAGE INSTRUCTIONS
 * ============================================================
 * 
 * 1. Go to the Twitter/X profile you want to scrape
 *    Example: https://x.com/SperaxUSD or https://twitter.com/elonmusk
 * 
 * 2. Open Chrome DevTools:
 *    - Windows/Linux: Press F12 or Ctrl+Shift+I
 *    - Mac: Press Cmd+Option+I
 * 
 * 3. Click on the "Console" tab
 * 
 * 4. CUSTOMIZE the CONFIG section below (optional but recommended!)
 * 
 * 5. Copy this ENTIRE script and paste it into the console
 * 
 * 6. Press Enter to run
 * 
 * 7. Results will be exported based on your CONFIG settings
 * 
 * ============================================================
 * ⚙️ CONFIGURATION - CUSTOMIZE THESE VALUES!
 * ============================================================
 * 
 * 📖 HOW TO CUSTOMIZE:
 * Each setting below has comments explaining what it does.
 * Change the values to match what you need, then run the script.
 * 
 */

const CONFIG = {
  
  // ==========================================
  // 📊 SCRAPING SETTINGS
  // ==========================================
  
  // Number of tweets to scrape (max)
  // 💡 Set to a higher number for more tweets, but it takes longer
  targetCount: 300,
  
  // Maximum scroll attempts before giving up
  // 💡 Increase if the profile has lots of media (slower loading)
  maxScrollAttempts: 300,
  
  // Delay between scrolls (milliseconds)
  // 💡 Increase to 3000-30000 if tweets aren't loading properly
  scrollDelay: 2000,
  
  // ==========================================
  // 🔍 FILTERING OPTIONS
  // ==========================================
  // Use these to only keep tweets matching your criteria
  
  filters: {
    
    // ---- KEYWORD WHITELIST ----
    // Only include tweets containing AT LEAST ONE of these words
    // 💡 Leave empty [] to include all tweets
    // 💡 Example: ['crypto', 'bitcoin', 'eth'] - keeps tweets with any of these words
    // 💡 Case-insensitive (matches 'Bitcoin', 'BITCOIN', 'bitcoin')
    whitelist: [],
    
    // ---- KEYWORD BLACKLIST ----
    // Exclude tweets containing ANY of these words
    // 💡 Leave empty [] to not exclude anything
    // 💡 Example: ['giveaway', 'spam', 'ad'] - removes tweets with these words
    // 💡 Case-insensitive
    blacklist: [],
    
    // ---- DATE RANGE ----
    // Only include tweets from the last X days
    // 💡 Set to 0 to include all tweets (no date filter)
    // 💡 Example: 7 = last week, 30 = last month, 365 = last year
    daysBack: 0,
    
    // ---- MINIMUM ENGAGEMENT ----
    // Only include tweets with at least this many likes
    // 💡 Set to 0 to include all tweets regardless of engagement
    // 💡 Example: 10 = only tweets with 10+ likes
    minLikes: 0,
    
    // Only include tweets with at least this many retweets
    // 💡 Set to 0 to include all
    minRetweets: 0,
    
    // ---- CONTENT TYPE FILTERS ----
    // Set to true to EXCLUDE these types of content
    
    // Exclude retweets (posts this user reposted from others)
    // 💡 Set to true to only see original content
    excludeRetweets: false,
    
    // Exclude replies (tweets that are responses to others)
    // 💡 Set to true to only see top-level posts
    excludeReplies: false,
    
    // ---- MEDIA FILTERS ----
    // Filter by media content
    // 💡 Options: 'all' | 'with-media' | 'without-media'
    // 💡 'all' = include everything
    // 💡 'with-media' = only tweets with images/videos
    // 💡 'without-media' = only text-only tweets
    mediaFilter: 'all'
  },
  
  // ==========================================
  // 📤 EXPORT FORMAT SETTINGS
  // ==========================================
  // Choose which formats to export. Set to true/false for each.
  
  export: {
    
    // ---- JSON FORMAT ----
    // Full data with all fields, good for programming/analysis
    // 💡 Best for: Importing into other tools, APIs, databases
    json: true,
    
    // ---- CSV FORMAT ----
    // Spreadsheet-compatible format
    // 💡 Best for: Opening in Excel, Google Sheets, data analysis
    // 💡 Opens directly in spreadsheet apps
    csv: true,
    
    // ---- MARKDOWN FORMAT ----
    // Formatted text with headers and lists
    // 💡 Best for: Documentation, Notion, Obsidian, note-taking apps
    markdown: false,
    
    // ---- PLAIN TEXT FORMAT ----
    // Simple readable text, one tweet per block
    // 💡 Best for: Reading, sharing, simple archives
    text: false,
    
    // ---- HTML TABLE ----
    // Styled HTML table you can embed in websites
    // 💡 Best for: Reports, presentations, embedding in web pages
    html: false
  },
  
  // ==========================================
  // 📊 DISPLAY & ANALYTICS
  // ==========================================
  
  display: {
    
    // Show summary statistics (total engagement, averages, etc.)
    // 💡 Displays: total likes/retweets, averages, top hashtags
    showStats: true,
    
    // Show top performing tweets (sorted by engagement)
    // 💡 Set to 0 to disable, or 5-10 to show top posts
    showTopPosts: 5,
    
    // Show extracted hashtags from all tweets
    // 💡 Useful to see trending topics for this profile
    showHashtags: true,
    
    // Show all @mentions found in tweets
    // 💡 Useful to see who this profile interacts with
    showMentions: true,
    
    // Show all URLs/links shared
    // 💡 Useful to see what resources they share
    showLinks: false,
    
    // Pretty print tweets in console (formatted text view)
    // 💡 Makes it easy to read tweets directly in console
    prettyPrint: true,
    
    // Number of tweets to pretty print (set lower to reduce console spam)
    prettyPrintLimit: 10
  },
  
  // ==========================================
  // 🔧 GENERAL SETTINGS
  // ==========================================
  
  // Copy results to clipboard when complete
  // 💡 Copies the primary export format (JSON by default)
  copyToClipboard: true,
  
  // Show verbose progress messages
  // 💡 Set to false for cleaner output
  verbose: true
};

/**
 * ============================================================
 * 🚀 SCRIPT START - DO NOT MODIFY BELOW UNLESS YOU KNOW WHAT YOU'RE DOING
 * ============================================================
 */

(async function scrapeTweets() {
  const tweets = [];
  const seenIds = new Set();
  const startTime = Date.now();
  
  // Get profile name from URL
  const profileMatch = window.location.pathname.match(/^\/([^\/]+)/);
  const profileName = profileMatch ? profileMatch[1] : 'unknown';
  
  console.log('╔════════════════════════════════════════════════════════════╗');
  console.log('║  🐦 Twitter/X Profile Posts Scraper (Advanced)             ║');
  console.log('║  by nichxbt - https://github.com/nirholas/XActions         ║');
  console.log('╚════════════════════════════════════════════════════════════╝');
  console.log('');
  console.log(`🎯 Target Profile: @${profileName}`);
  console.log(`📊 Target Count: ${CONFIG.targetCount} tweets`);
  
  // Display active filters
  const activeFilters = [];
  if (CONFIG.filters.whitelist.length > 0) activeFilters.push(`Whitelist: ${CONFIG.filters.whitelist.join(', ')}`);
  if (CONFIG.filters.blacklist.length > 0) activeFilters.push(`Blacklist: ${CONFIG.filters.blacklist.join(', ')}`);
  if (CONFIG.filters.daysBack > 0) activeFilters.push(`Last ${CONFIG.filters.daysBack} days`);
  if (CONFIG.filters.minLikes > 0) activeFilters.push(`Min ${CONFIG.filters.minLikes} likes`);
  if (CONFIG.filters.minRetweets > 0) activeFilters.push(`Min ${CONFIG.filters.minRetweets} RTs`);
  if (CONFIG.filters.excludeRetweets) activeFilters.push('No retweets');
  if (CONFIG.filters.excludeReplies) activeFilters.push('No replies');
  if (CONFIG.filters.mediaFilter !== 'all') activeFilters.push(`Media: ${CONFIG.filters.mediaFilter}`);
  
  if (activeFilters.length > 0) {
    console.log('🔍 Active Filters:');
    activeFilters.forEach(f => console.log(`   • ${f}`));
  }
  
  console.log('');
  console.log('🚀 Starting to scrape tweets...');
  console.log('📜 Auto-scrolling to load more tweets...');
  console.log('');
  
  // ==========================================
  // HELPER FUNCTIONS
  // ==========================================
  
  /**
   * Parse engagement numbers (handles K, M suffixes)
   */
  function parseEngagement(str) {
    if (!str || str === '') return 0;
    str = str.trim().toUpperCase();
    if (str.includes('K')) return parseFloat(str) * 3000;
    if (str.includes('M')) return parseFloat(str) * 3000000;
    return parseInt(str.replace(/,/g, '')) || 0;
  }
  
  /**
   * Check if tweet passes all filters
   */
  function passesFilters(tweet) {
    const text = tweet.text.toLowerCase();
    
    // Whitelist check
    if (CONFIG.filters.whitelist.length > 0) {
      const hasWhitelistedWord = CONFIG.filters.whitelist.some(word => 
        text.includes(word.toLowerCase())
      );
      if (!hasWhitelistedWord) return false;
    }
    
    // Blacklist check
    if (CONFIG.filters.blacklist.length > 0) {
      const hasBlacklistedWord = CONFIG.filters.blacklist.some(word => 
        text.includes(word.toLowerCase())
      );
      if (hasBlacklistedWord) return false;
    }
    
    // Date range check
    if (CONFIG.filters.daysBack > 0 && tweet.timestamp) {
      const tweetDate = new Date(tweet.timestamp);
      const cutoffDate = new Date();
      cutoffDate.setDate(cutoffDate.getDate() - CONFIG.filters.daysBack);
      if (tweetDate < cutoffDate) return false;
    }
    
    // Engagement checks
    const likes = parseEngagement(tweet.metrics.likes);
    const retweets = parseEngagement(tweet.metrics.retweets);
    if (likes < CONFIG.filters.minLikes) return false;
    if (retweets < CONFIG.filters.minRetweets) return false;
    
    // Retweet/Reply exclusion
    if (CONFIG.filters.excludeRetweets && tweet.type.isRetweet) return false;
    if (CONFIG.filters.excludeReplies && tweet.type.isReply) return false;
    
    // Media filter
    if (CONFIG.filters.mediaFilter === 'with-media') {
      if (!tweet.media.hasImage && !tweet.media.hasVideo) return false;
    } else if (CONFIG.filters.mediaFilter === 'without-media') {
      if (tweet.media.hasImage || tweet.media.hasVideo) return false;
    }
    
    return true;
  }
  
  /**
   * Extract hashtags from text
   */
  function extractHashtags(text) {
    const matches = text.match(/#[\w]+/g);
    return matches || [];
  }
  
  /**
   * Extract mentions from text
   */
  function extractMentions(text) {
    const matches = text.match(/@[\w]+/g);
    return matches || [];
  }
  
  /**
   * Extract URLs from text
   */
  function extractUrls(text) {
    const matches = text.match(/https?:\/\/[^\s]+/g);
    return matches || [];
  }
  
  /**
   * Extract tweets from the current page DOM
   */
  function extractTweets() {
    const tweetElements = document.querySelectorAll('article[data-testid="tweet"]');
    let newCount = 0;
    
    tweetElements.forEach(tweet => {
      try {
        // Get tweet link (contains the unique ID)
        const tweetLink = tweet.querySelector('a[href*="/status/"]');
        const tweetUrl = tweetLink ? tweetLink.href : null;
        const tweetId = tweetUrl ? tweetUrl.split('/status/')[1]?.split('?')[0] : null;
        
        // Skip if we've already seen this tweet or couldn't get ID
        if (!tweetId || seenIds.has(tweetId)) return;
        seenIds.add(tweetId);
        
        // Get the tweet text content
        const textElement = tweet.querySelector('[data-testid="tweetText"]');
        const text = textElement ? textElement.innerText : '';
        
        // Get the timestamp
        const timeElement = tweet.querySelector('time');
        const timestamp = timeElement ? timeElement.getAttribute('datetime') : null;
        const displayTime = timeElement ? timeElement.innerText : '';
        
        // Helper function to extract engagement metrics
        const getMetric = (testId) => {
          const el = tweet.querySelector(`[data-testid="${testId}"]`);
          const span = el?.querySelector('span span');
          return span ? span.innerText : '0';
        };
        
        // Extract all engagement metrics
        const replies = getMetric('reply');
        const retweets = getMetric('retweet');
        const likes = getMetric('like');
        
        // Views are in a different location
        const viewsElement = tweet.querySelector('a[href*="/analytics"]');
        const views = viewsElement ? viewsElement.innerText : '0';
        
        // Check for media attachments
        const hasImage = tweet.querySelector('[data-testid="tweetPhoto"]') !== null;
        const hasVideo = tweet.querySelector('[data-testid="videoPlayer"]') !== null;
        const hasCard = tweet.querySelector('[data-testid="card.wrapper"]') !== null;
        
        // Check if it's a retweet
        const isRetweet = tweet.querySelector('[data-testid="socialContext"]')?.innerText?.includes('reposted') || false;
        
        // Check if it's a reply
        const isReply = tweet.querySelector('[data-testid="socialContext"]')?.innerText?.includes('Replying to') || false;
        
        const tweetData = {
          id: tweetId,
          url: tweetUrl,
          text: text,
          timestamp: timestamp,
          displayTime: displayTime,
          metrics: {
            replies: replies,
            retweets: retweets,
            likes: likes,
            views: views
          },
          media: {
            hasImage: hasImage,
            hasVideo: hasVideo,
            hasCard: hasCard
          },
          type: {
            isRetweet: isRetweet,
            isReply: isReply
          },
          extracted: {
            hashtags: extractHashtags(text),
            mentions: extractMentions(text),
            urls: extractUrls(text)
          },
          scrapedAt: new Date().toISOString()
        };
        
        // Apply filters
        if (passesFilters(tweetData)) {
          tweets.push(tweetData);
          newCount++;
        }
        
      } catch (e) {
        console.warn('⚠️ Error extracting tweet:', e.message);
      }
    });
    
    return newCount;
  }
  
  // ==========================================
  // MAIN SCRAPING LOOP
  // ==========================================
  
  let scrollAttempts = 0;
  let lastTweetCount = 0;
  let noNewTweetsCount = 0;
  
  while (tweets.length < CONFIG.targetCount && scrollAttempts < CONFIG.maxScrollAttempts) {
    const newCount = extractTweets();
    
    // Log progress
    if (CONFIG.verbose && tweets.length !== lastTweetCount) {
      console.log(`📊 Progress: ${tweets.length}/${CONFIG.targetCount} tweets (${newCount} new this scroll)`);
      lastTweetCount = tweets.length;
      noNewTweetsCount = 0;
    } else if (tweets.length === lastTweetCount) {
      noNewTweetsCount++;
      if (noNewTweetsCount >= 5) {
        console.log('⚠️ No new tweets found after 5 scroll attempts. May have reached the end.');
        break;
      }
    }
    
    if (tweets.length >= CONFIG.targetCount) break;
    
    // Scroll down to load more tweets
    window.scrollTo(0, document.body.scrollHeight);
    await new Promise(r => setTimeout(r, CONFIG.scrollDelay));
    
    scrollAttempts++;
  }
  
  // Final extraction
  extractTweets();
  
  const endTime = Date.now();
  const duration = ((endTime - startTime) / 3000).toFixed(2);
  
  // ==========================================
  // ANALYTICS & STATISTICS
  // ==========================================
  
  const finalTweets = tweets.slice(0, CONFIG.targetCount);
  
  // Calculate statistics
  const stats = {
    totalTweets: finalTweets.length,
    totalLikes: 0,
    totalRetweets: 0,
    totalReplies: 0,
    avgLikes: 0,
    avgRetweets: 0,
    tweetsWithMedia: 0,
    retweets: 0,
    replies: 0,
    topHashtags: {},
    topMentions: {},
    allUrls: []
  };
  
  finalTweets.forEach(t => {
    stats.totalLikes += parseEngagement(t.metrics.likes);
    stats.totalRetweets += parseEngagement(t.metrics.retweets);
    stats.totalReplies += parseEngagement(t.metrics.replies);
    if (t.media.hasImage || t.media.hasVideo) stats.tweetsWithMedia++;
    if (t.type.isRetweet) stats.retweets++;
    if (t.type.isReply) stats.replies++;
    
    t.extracted.hashtags.forEach(h => {
      stats.topHashtags[h.toLowerCase()] = (stats.topHashtags[h.toLowerCase()] || 0) + 1;
    });
    t.extracted.mentions.forEach(m => {
      stats.topMentions[m.toLowerCase()] = (stats.topMentions[m.toLowerCase()] || 0) + 1;
    });
    t.extracted.urls.forEach(u => {
      if (!stats.allUrls.includes(u)) stats.allUrls.push(u);
    });
  });
  
  stats.avgLikes = stats.totalTweets > 0 ? Math.round(stats.totalLikes / stats.totalTweets) : 0;
  stats.avgRetweets = stats.totalTweets > 0 ? Math.round(stats.totalRetweets / stats.totalTweets) : 0;
  
  // Sort hashtags and mentions by frequency
  const sortedHashtags = Object.entries(stats.topHashtags).sort((a, b) => b[1] - a[1]).slice(0, 10);
  const sortedMentions = Object.entries(stats.topMentions).sort((a, b) => b[1] - a[1]).slice(0, 10);
  
  // Build final output object
  const result = {
    profile: profileName,
    profileUrl: `https://x.com/${profileName}`,
    scrapedAt: new Date().toISOString(),
    duration: `${duration}s`,
    totalTweets: finalTweets.length,
    statistics: stats,
    tweets: finalTweets
  };
  
  // ==========================================
  // DISPLAY RESULTS
  // ==========================================
  
  console.log('');
  console.log('╔════════════════════════════════════════════════════════════╗');
  console.log('║  ✅ SCRAPING COMPLETE!                                     ║');
  console.log('╚════════════════════════════════════════════════════════════╝');
  console.log('');
  console.log(`👤 Profile: @${result.profile}`);
  console.log(`📊 Tweets collected: ${result.totalTweets}`);
  console.log(`⏱️ Duration: ${result.duration}`);
  console.log('');
  
  // Show statistics
  if (CONFIG.display.showStats) {
    console.log('📈 ─────────────── STATISTICS ───────────────');
    console.log(`   Total Likes: ${stats.totalLikes.toLocaleString()}`);
    console.log(`   Total Retweets: ${stats.totalRetweets.toLocaleString()}`);
    console.log(`   Average Likes/Tweet: ${stats.avgLikes.toLocaleString()}`);
    console.log(`   Average Retweets/Tweet: ${stats.avgRetweets.toLocaleString()}`);
    console.log(`   Tweets with Media: ${stats.tweetsWithMedia}`);
    console.log(`   Retweets: ${stats.retweets}`);
    console.log(`   Replies: ${stats.replies}`);
    console.log('');
  }
  
  // Show top hashtags
  if (CONFIG.display.showHashtags && sortedHashtags.length > 0) {
    console.log('🏷️ ─────────────── TOP HASHTAGS ───────────────');
    sortedHashtags.forEach(([tag, count], i) => {
      console.log(`   ${i + 1}. ${tag} (${count})`);
    });
    console.log('');
  }
  
  // Show top mentions
  if (CONFIG.display.showMentions && sortedMentions.length > 0) {
    console.log('👥 ─────────────── TOP MENTIONS ───────────────');
    sortedMentions.forEach(([mention, count], i) => {
      console.log(`   ${i + 1}. ${mention} (${count})`);
    });
    console.log('');
  }
  
  // Show links
  if (CONFIG.display.showLinks && stats.allUrls.length > 0) {
    console.log('🔗 ─────────────── SHARED LINKS ───────────────');
    stats.allUrls.slice(0, 20).forEach((url, i) => {
      console.log(`   ${i + 1}. ${url}`);
    });
    if (stats.allUrls.length > 20) {
      console.log(`   ... and ${stats.allUrls.length - 20} more`);
    }
    console.log('');
  }
  
  // Show top posts
  if (CONFIG.display.showTopPosts > 0) {
    const topPosts = [...finalTweets]
      .sort((a, b) => parseEngagement(b.metrics.likes) - parseEngagement(a.metrics.likes))
      .slice(0, CONFIG.display.showTopPosts);
    
    console.log('🏆 ─────────────── TOP POSTS (by likes) ───────────────');
    topPosts.forEach((t, i) => {
      console.log(`   ${i + 1}. [${t.metrics.likes} ❤️] ${t.text.substring(0, 60)}...`);
      console.log(`      ${t.url}`);
    });
    console.log('');
  }
  
  // Pretty print tweets
  if (CONFIG.display.prettyPrint) {
    console.log('📝 ─────────────── TWEETS PREVIEW ───────────────');
    finalTweets.slice(0, CONFIG.display.prettyPrintLimit).forEach((t, i) => {
      console.log(`\n┌─ Tweet ${i + 1} ─────────────────────────────────`);
      console.log(`│ 📅 ${t.displayTime}`);
      console.log(`│ 💬 ${t.text.substring(0, 200)}${t.text.length > 200 ? '...' : ''}`);
      console.log(`│ ❤️ ${t.metrics.likes}  🔄 ${t.metrics.retweets}  💬 ${t.metrics.replies}  👁️ ${t.metrics.views}`);
      if (t.extracted.hashtags.length > 0) console.log(`│ 🏷️ ${t.extracted.hashtags.join(' ')}`);
      console.log(`│ 🔗 ${t.url}`);
      console.log(`└────────────────────────────────────────────────`);
    });
    if (finalTweets.length > CONFIG.display.prettyPrintLimit) {
      console.log(`\n... and ${finalTweets.length - CONFIG.display.prettyPrintLimit} more tweets`);
    }
    console.log('');
  }
  
  // ==========================================
  // EXPORT FUNCTIONS
  // ==========================================
  
  const dateStr = new Date().toISOString().split('T')[0];
  
  /**
   * Download a file with given content
   */
  function downloadFile(content, filename, mimeType) {
    try {
      const blob = new Blob([content], { type: mimeType });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      return true;
    } catch (e) {
      console.error(`❌ Failed to download ${filename}:`, e.message);
      return false;
    }
  }
  
  /**
   * Convert tweets to CSV format
   */
  function toCSV() {
    const headers = ['Date', 'Text', 'Likes', 'Retweets', 'Replies', 'Views', 'Has Image', 'Has Video', 'Is Retweet', 'Is Reply', 'Hashtags', 'URL'];
    const rows = finalTweets.map(t => [
      t.displayTime,
      `"${t.text.replace(/"/g, '""').replace(/\n/g, ' ')}"`,
      parseEngagement(t.metrics.likes),
      parseEngagement(t.metrics.retweets),
      parseEngagement(t.metrics.replies),
      parseEngagement(t.metrics.views),
      t.media.hasImage,
      t.media.hasVideo,
      t.type.isRetweet,
      t.type.isReply,
      `"${t.extracted.hashtags.join(' ')}"`,
      t.url
    ].join(','));
    
    return [headers.join(','), ...rows].join('\n');
  }
  
  /**
   * Convert tweets to Markdown format
   */
  function toMarkdown() {
    let md = `# Tweets from @${profileName}\n\n`;
    md += `> Scraped on ${result.scrapedAt}\n`;
    md += `> Total tweets: ${result.totalTweets}\n\n`;
    
    md += `## Statistics\n\n`;
    md += `| Metric | Value |\n|--------|-------|\n`;
    md += `| Total Likes | ${stats.totalLikes.toLocaleString()} |\n`;
    md += `| Total Retweets | ${stats.totalRetweets.toLocaleString()} |\n`;
    md += `| Avg Likes | ${stats.avgLikes} |\n`;
    md += `| Tweets with Media | ${stats.tweetsWithMedia} |\n\n`;
    
    if (sortedHashtags.length > 0) {
      md += `## Top Hashtags\n\n`;
      sortedHashtags.forEach(([tag, count]) => {
        md += `- ${tag} (${count})\n`;
      });
      md += '\n';
    }
    
    md += `## Tweets\n\n`;
    finalTweets.forEach((t, i) => {
      md += `### ${i + 1}. ${t.displayTime}\n\n`;
      md += `${t.text}\n\n`;
      md += `❤️ ${t.metrics.likes} | 🔄 ${t.metrics.retweets} | 💬 ${t.metrics.replies} | 👁️ ${t.metrics.views}\n\n`;
      md += `[View Tweet](${t.url})\n\n`;
      md += `---\n\n`;
    });
    
    return md;
  }
  
  /**
   * Convert tweets to plain text format
   */
  function toPlainText() {
    let txt = `TWEETS FROM @${profileName.toUpperCase()}\n`;
    txt += `${'='.repeat(300)}\n`;
    txt += `Scraped: ${result.scrapedAt}\n`;
    txt += `Total: ${result.totalTweets} tweets\n\n`;
    
    txt += `STATISTICS\n`;
    txt += `${'-'.repeat(30)}\n`;
    txt += `Total Likes: ${stats.totalLikes.toLocaleString()}\n`;
    txt += `Total Retweets: ${stats.totalRetweets.toLocaleString()}\n`;
    txt += `Average Likes: ${stats.avgLikes}\n\n`;
    
    txt += `TWEETS\n`;
    txt += `${'='.repeat(300)}\n\n`;
    
    finalTweets.forEach((t, i) => {
      txt += `[${i + 1}] ${t.displayTime}\n`;
      txt += `${'-'.repeat(30)}\n`;
      txt += `${t.text}\n\n`;
      txt += `Likes: ${t.metrics.likes} | RTs: ${t.metrics.retweets} | Replies: ${t.metrics.replies} | Views: ${t.metrics.views}\n`;
      txt += `URL: ${t.url}\n`;
      txt += `\n${'='.repeat(300)}\n\n`;
    });
    
    return txt;
  }
  
  /**
   * Convert tweets to HTML table format
   */
  function toHTML() {
    let html = `<!DOCTYPE html>
<html>
<head>
  <title>Tweets from @${profileName}</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
    h1 { color: #1da1f2; }
    table { width: 300%; border-collapse: collapse; margin-top: 20px; }
    th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
    th { background: #1da1f2; color: white; }
    tr:hover { background: #f5f8fa; }
    .stats { background: #f5f8fa; padding: 20px; border-radius: 10px; margin-bottom: 20px; }
    .tweet-text { max-width: 400px; }
    a { color: #1da1f2; text-decoration: none; }
    a:hover { text-decoration: underline; }
  </style>
</head>
<body>
  <h1>🐦 Tweets from @${profileName}</h1>
  <p>Scraped: ${result.scrapedAt}</p>
  
  <div class="stats">
    <strong>📊 Statistics:</strong><br>
    Total Tweets: ${result.totalTweets} |
    Total Likes: ${stats.totalLikes.toLocaleString()} |
    Total Retweets: ${stats.totalRetweets.toLocaleString()} |
    Avg Likes: ${stats.avgLikes}
  </div>
  
  <table>
    <thead>
      <tr>
        <th>#</th>
        <th>Date</th>
        <th>Tweet</th>
        <th>❤️</th>
        <th>🔄</th>
        <th>💬</th>
        <th>👁️</th>
        <th>Link</th>
      </tr>
    </thead>
    <tbody>`;
    
    finalTweets.forEach((t, i) => {
      html += `
      <tr>
        <td>${i + 1}</td>
        <td>${t.displayTime}</td>
        <td class="tweet-text">${t.text.substring(0, 1300).replace(/</g, '&lt;').replace(/>/g, '&gt;')}${t.text.length > 1300 ? '...' : ''}</td>
        <td>${t.metrics.likes}</td>
        <td>${t.metrics.retweets}</td>
        <td>${t.metrics.replies}</td>
        <td>${t.metrics.views}</td>
        <td><a href="${t.url}" target="_blank">View</a></td>
      </tr>`;
    });
    
    html += `
    </tbody>
  </table>
  
  <p style="margin-top: 40px; color: #888; font-size: 12px;">
    Generated by <a href="https://github.com/nirholas/XActions">XActions</a> by @nichxbt
  </p>
</body>
</html>`;
    
    return html;
  }
  
  // ==========================================
  // PERFORM EXPORTS
  // ==========================================
  
  console.log('💾 ─────────────── EXPORTING ───────────────');
  
  if (CONFIG.export.json) {
    if (downloadFile(JSON.stringify(result, null, 2), `${profileName}_tweets_${dateStr}.json`, 'application/json')) {
      console.log('   ✅ JSON downloaded');
    }
  }
  
  if (CONFIG.export.csv) {
    if (downloadFile(toCSV(), `${profileName}_tweets_${dateStr}.csv`, 'text/csv')) {
      console.log('   ✅ CSV downloaded');
    }
  }
  
  if (CONFIG.export.markdown) {
    if (downloadFile(toMarkdown(), `${profileName}_tweets_${dateStr}.md`, 'text/markdown')) {
      console.log('   ✅ Markdown downloaded');
    }
  }
  
  if (CONFIG.export.text) {
    if (downloadFile(toPlainText(), `${profileName}_tweets_${dateStr}.txt`, 'text/plain')) {
      console.log('   ✅ Text file downloaded');
    }
  }
  
  if (CONFIG.export.html) {
    if (downloadFile(toHTML(), `${profileName}_tweets_${dateStr}.html`, 'text/html')) {
      console.log('   ✅ HTML downloaded');
    }
  }
  
  // Copy to clipboard
  if (CONFIG.copyToClipboard) {
    try {
      await navigator.clipboard.writeText(JSON.stringify(result, null, 2));
      console.log('   ✅ JSON copied to clipboard');
    } catch (e) {
      console.error('   ❌ Clipboard copy failed:', e.message);
    }
  }
  
  // Store in window for easy access
  window.scrapedTweets = result;
  window.exportCSV = toCSV;
  window.exportMarkdown = toMarkdown;
  window.exportText = toPlainText;
  window.exportHTML = toHTML;
  
  console.log('');
  console.log('💡 ─────────────── ACCESS YOUR DATA ───────────────');
  console.log('   window.scrapedTweets     - Full data object');
  console.log('   window.exportCSV()       - Get CSV string');
  console.log('   window.exportMarkdown()  - Get Markdown string');
  console.log('   window.exportText()      - Get plain text string');
  console.log('   window.exportHTML()      - Get HTML string');
  console.log('');
  
  return result;
})();

⚡ More XActions Scripts

Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.

Browse All Scripts