One console script that opens a searchable menu of every XActions tool (scrape, analyze, grow, engage, clean up, and moderate) with per-tool options and one-click run.
How to Use
- Navigate to x.com and log in
- Open DevTools Console (F12 or Cmd+Option+I)
- Paste the script below and press Enter
Default Configuration
const CONFIG = {
settings: {
// true = protect your posts (private account), false = public.
// protectPosts: true,
// true = allow others to tag you in photos, false = turn photo tagging off.
// photoTagging: false,
// true = people with your email can find you, false = they cannot.
// discoverableByEmail: false,
// Who can send you message requests: 'everyone' | 'verified' | 'no-one'.
// allowDMsFrom: 'verified',
},
// Delay for UI interactions (ms).
actionDelay: 900
};
Full Script
Copy and paste this entire script into your browser DevTools console on x.com.
// Copyright (c) 2024-2026 nich (@nichxbt). All rights reserved.
/**
* ============================================================
* ⚡ XActions Command Center (v1.0.0)
* ============================================================
* The one script to run them all. Paste this into your browser's DevTools
* console on x.com and a searchable command palette appears with every
* XActions tool (108 of them): scrape, analyze, grow, engage,
* clean up, moderate, and more. Pick a tool, set its options, press Run.
*
* 1. Open x.com and press F12 (or Cmd+Option+I) → Console tab.
* 2. Paste this entire file and press Enter.
* 3. Search, choose a tool, and click Run. Reopen anytime with the
* ⚡ button (bottom-right) or Cmd/Ctrl+K.
*
* @name XActions Command Center
* @description One console script that opens a searchable menu of every XActions tool (scrape, analyze, grow, engage, clean up, and moderate) with per-tool options and one-click run.
* @version 1.0.0
* @author nichxbt (https://x.com/nichxbt)
*
* GENERATED FILE. Do not edit by hand. Source: scripts/twitter/_command-center-shell.js
* + scripts/build-toolkit.mjs. Regenerate with: node scripts/build-toolkit.mjs
* @repository https://github.com/nirholas/XActions
*/
/*
* ============================================================
* SOURCE SHELL for xactions-command-center.js (do not paste this file)
* ============================================================
* This is the UI shell for the XActions Command Center. It is NOT the
* script users run. `scripts/build-toolkit.mjs` injects the tool catalog
* and every bundled tool at the __XA_INJECT_DATA__ marker below and writes
* the runnable result to scripts/twitter/xactions-command-center.js.
*
* To change the launcher UI, edit THIS file, then run:
* node scripts/build-toolkit.mjs
*
* The injected data provides three bindings this shell relies on:
* CATALOG - array of tool metadata { id, title, emoji, category,
* danger, desc, where, defaults, stopGlobal }
* CATEGORIES - ordered array of { id, label, emoji }
* TOOLS - object mapping tool id -> function that runs the tool
*/
(function xactionsCommandCenter() {
// Intentionally NOT in strict mode: bundled tools are pasted verbatim and
// some rely on sloppy-mode semantics exactly as they do when run standalone.
// Re-pasting the script replaces any live instance cleanly.
if (window.XActionsCommandCenter && typeof window.XActionsCommandCenter.destroy === 'function') {
try { window.XActionsCommandCenter.destroy(); } catch (e) { /* ignore */ }
}
const CATALOG = [{"id":"create-poll","title":"Create a Poll","emoji":"📊","category":"create","danger":"caution","desc":"Publish a poll with your choices and duration.","where":{"label":"The post composer","url":"https://x.com/compose/post","match":["^/compose"]},"defaults":{"text":"What should we build next?","choices":["Solana tools","Base tools","More avatars"],"durationDays":1,"durationHours":0,"durationMinutes":0,"dryRun":true},"stopGlobal":null},{"id":"pin-tweet","title":"Pin / Unpin Tweet","emoji":"📌","category":"create","danger":"caution","desc":"Pin or unpin one of your own tweets.","where":{"label":"Your own profile page"},"defaults":{"action":"pin","tweetUrl":"","dryRun":true},"stopGlobal":null},{"id":"post-thread","title":"Post a Thread","emoji":"🧵","category":"create","danger":"caution","desc":"Publish a multi-tweet thread from a list of texts.","where":{"label":"The post composer","url":"https://x.com/compose/post","match":["^/compose"]},"defaults":{"tweets":["Thread part 1 🧵","Thread part 2: the details.","Thread part 3: wrap up. Follow for more."],"autoNumber":false,"maxChars":280,"delayBetween":2000,"dryRun":true},"stopGlobal":null},{"id":"post-tweet","title":"Post a Tweet","emoji":"✍️","category":"create","danger":"caution","desc":"Compose and publish a single tweet from the console.","where":{"label":"The post composer","url":"https://x.com/compose/post","match":["^/compose"]},"defaults":{"text":"gm from XActions 🚀","replyToUrl":"","mediaNote":"","mediaWaitSeconds":15,"dryRun":true},"stopGlobal":null},{"id":"quote-tweet","title":"Quote Tweet","emoji":"💬","category":"create","danger":"caution","desc":"Quote-tweet the post you are viewing with your own text.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"text":"This is the take of the day 💯","dryRun":true},"stopGlobal":null},{"id":"schedule-post","title":"Schedule a Post","emoji":"🗓️","category":"create","danger":"caution","desc":"Schedule a tweet using X's native scheduler.","where":{"label":"The post composer","url":"https://x.com/compose/post","match":["^/compose"]},"defaults":{"text":"Scheduled with XActions 📅","scheduleISO":"2026-08-01T09:00","dryRun":true},"stopGlobal":null},{"id":"backup-account","title":"Account Backup","emoji":"💾","category":"scrape","danger":"safe","desc":"Make a comprehensive backup of your account data.","where":{"label":"Your own profile page"},"defaults":{"maxTweets":100,"maxLikes":100,"maxBookmarks":100,"maxFollowing":500,"maxFollowers":500,"scrollDelay":2000,"autoDownload":true},"stopGlobal":null},{"id":"bookmark-exporter","title":"Bookmark Exporter","emoji":"🔖","category":"scrape","danger":"safe","desc":"Export all of your bookmarks to JSON and CSV.","where":{"label":"Your Bookmarks","url":"https://x.com/i/bookmarks","match":["^/i/bookmarks"]},"defaults":{"maxBookmarks":1000,"scrollDelay":1500,"maxScrolls":200,"maxRetries":5,"exportJSON":true,"exportCSV":true,"copyToClipboard":true},"stopGlobal":null},{"id":"link-scraper","title":"Link Scraper","emoji":"🔗","category":"scrape","danger":"safe","desc":"Extract every external link a user has shared.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"maxScrolls":100,"maxTweets":500,"scrollDelay":1500,"includeTwitterLinks":false,"includeMedia":false,"excludeDomains":["t.co"],"autoDownload":true,"maxRetries":5},"stopGlobal":null},{"id":"scrape-hashtag","title":"Scrape a Hashtag","emoji":"#️⃣","category":"scrape","danger":"safe","desc":"Export posts for a hashtag.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"hashtag":"","maxPosts":300,"includeReplies":true,"scrollDelay":1800,"noNewPostsThreshold":6,"maxScrollAttempts":400,"downloadCsv":true},"stopGlobal":"stopScrapeHashtag"},{"id":"scrape-list","title":"Scrape a List","emoji":"📋","category":"scrape","danger":"safe","desc":"Export a List’s timeline or members.","where":{"label":"A List page (x.com/i/lists/<id>)","match":["/lists/"]},"defaults":{"mode":"auto","maxItems":500,"includeReplies":true,"scrollDelay":1800,"noNewItemsThreshold":6,"maxScrollAttempts":500,"downloadCsv":true},"stopGlobal":"stopScrapeList"},{"id":"scrape-user-likes","title":"Scrape a User's Likes","emoji":"💗","category":"scrape","danger":"safe","desc":"Export the posts a user has liked.","where":{"label":"Your Likes page (x.com/<you>/likes)","match":["/likes"]},"defaults":{"maxPosts":1000,"scrollStep":1600,"scrollDelay":1600,"stallLimit":6,"maxScrollAttempts":400,"autoDownloadCsv":false},"stopGlobal":"stopScrapeUserLikes"},{"id":"scrape-dms","title":"Scrape DMs","emoji":"✉️","category":"scrape","danger":"safe","desc":"Export the open DM conversation.","where":{"label":"Your Messages","url":"https://x.com/messages","match":["^/messages"]},"defaults":{"maxMessages":2000,"maxScrollAttempts":120,"noNewThreshold":6,"scrollDelay":1200,"downloadJSON":true,"downloadCSV":true},"stopGlobal":"stopScrapeDMs"},{"id":"scrape-followers","title":"Scrape Followers","emoji":"👥","category":"scrape","danger":"safe","desc":"Export a profile’s followers to JSON/CSV.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"maxUsers":5000,"scrollStep":1400,"scrollDelay":1500,"stallLimit":6,"maxScrollAttempts":400,"autoDownloadCsv":false},"stopGlobal":"stopScrapeFollowers"},{"id":"scrape-following","title":"Scrape Following","emoji":"👣","category":"scrape","danger":"safe","desc":"Export who a profile follows to JSON/CSV.","where":{"label":"Your Following page (x.com/<you>/following)","match":["/following"]},"defaults":{"maxUsers":5000,"scrollStep":1400,"scrollDelay":1500,"stallLimit":6,"maxScrollAttempts":400,"autoDownloadCsv":false},"stopGlobal":"stopScrapeFollowing"},{"id":"scrape-notifications","title":"Scrape Notifications","emoji":"🔔","category":"scrape","danger":"safe","desc":"Export your notifications feed.","where":{"label":"Your Notifications","url":"https://x.com/notifications","match":["^/notifications"]},"defaults":{"maxNotifications":500,"maxScrollAttempts":200,"noNewThreshold":5,"scrollDelay":1500,"downloadJSON":true,"downloadCSV":true},"stopGlobal":"stopScrapeNotifications"},{"id":"scrape-likers","title":"Scrape Post Likers","emoji":"❤️","category":"scrape","danger":"safe","desc":"Export the users who liked a post.","where":{"label":"A tweet's Likes page (.../likes)","match":["/status/.*likes","/likes"]},"defaults":{"maxUsers":5000,"scrollStep":1400,"scrollDelay":1500,"stallLimit":6,"maxScrollAttempts":400,"autoDownloadCsv":false},"stopGlobal":"stopScrapeLikers"},{"id":"scrape-profile-with-replies","title":"Scrape Posts + Replies","emoji":"🧵","category":"scrape","danger":"safe","desc":"Scrape a profile including its replies, from the With replies tab.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"targetPostCount":50,"maxRepliesPerPost":50,"maxFeedScrollAttempts":200,"maxThreadScrollAttempts":30,"scrollDelay":2000,"navigationDelay":3000,"range":{"startPostId":null,"endPostId":null},"filters":{"whitelist":[],"blacklist":[],"daysBack":0,"minLikes":0,"minRetweets":0,"excludeRetweets":false},"export":{"json":true,"csv":true,"markdown":false,"text":false,"html":false},"panel":{"enabled":true,"top":20,"right":20},"copyToClipboard":true,"verbose":true,"scrapeRepliesOnUserReplies":true},"stopGlobal":null},{"id":"scrape-media","title":"Scrape Profile Media","emoji":"🖼️","category":"scrape","danger":"safe","desc":"Export image and video URLs from a profile.","where":{"label":"A profile's Media tab","match":["/media"]},"defaults":{"maxItems":500,"highestRes":true,"includeImages":true,"includeVideos":true,"includeGifs":true,"scrollDelay":1800,"noNewItemsThreshold":6,"maxScrollAttempts":400,"downloadCsv":true,"downloadUrlList":true},"stopGlobal":"stopScrapeMedia"},{"id":"scrape-profile-posts","title":"Scrape Profile Posts","emoji":"📜","category":"scrape","danger":"safe","desc":"Scrape every post from any profile with filters, analytics, and JSON/CSV/MD exports.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"targetCount":500,"maxScrollAttempts":500,"scrollDelay":2000,"filters":{"whitelist":[],"blacklist":[],"daysBack":0,"minLikes":0,"minRetweets":0,"excludeRetweets":false,"excludeReplies":false,"mediaFilter":"all"},"export":{"json":true,"csv":true,"markdown":false,"text":false,"html":false},"display":{"showStats":true,"showTopPosts":10,"showHashtags":true,"showMentions":true,"showLinks":true,"prettyPrint":true,"prettyPrintLimit":10},"copyToClipboard":true,"verbose":true},"stopGlobal":null},{"id":"scrape-retweeters","title":"Scrape Reposters","emoji":"🔁","category":"scrape","danger":"safe","desc":"Export reposters and quote-tweeters of a post.","where":{"label":"A tweet's Reposts page (.../retweets)","match":["/status/.*(retweets|quotes)"]},"defaults":{"collectReposts":true,"collectQuotes":true,"maxEntries":5000,"scrollStep":1400,"scrollDelay":1500,"stallLimit":6,"maxScrollAttempts":300,"autoDownloadCsv":false},"stopGlobal":"stopScrapeReposters"},{"id":"scrape-search","title":"Scrape Search Results","emoji":"🔎","category":"scrape","danger":"safe","desc":"Export posts from a search query.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"maxPosts":300,"includeReplies":true,"scrollDelay":1800,"noNewPostsThreshold":6,"maxScrollAttempts":400,"downloadCsv":true},"stopGlobal":"stopScrapeSearch"},{"id":"scrape-spaces","title":"Scrape Spaces","emoji":"🎙️","category":"scrape","danger":"safe","desc":"Capture Spaces info from a profile or Space.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"maxSpaces":50,"maxScrollAttempts":60,"noNewThreshold":4,"scrollDelay":1500,"downloadJSON":true,"downloadCSV":true},"stopGlobal":"stopScrapeSpaces"},{"id":"scrape-replies","title":"Scrape Tweet Replies","emoji":"💬","category":"scrape","danger":"safe","desc":"Export the replies under a post.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"maxReplies":500,"scrollDelay":1800,"noNewRepliesThreshold":6,"maxScrollAttempts":400,"downloadCsv":true},"stopGlobal":"stopScrapeReplies"},{"id":"scraper-toolbox","title":"Scraper Toolbox","emoji":"🧰","category":"scrape","danger":"safe","desc":"Full on-page scraping control panel: start/pause/stop, live filters, one-click exports.","where":{"label":"Any X page"},"defaults":{},"stopGlobal":null},{"id":"thread-unroller","title":"Thread Unroller","emoji":"🪡","category":"scrape","danger":"safe","desc":"Save any thread as clean text, markdown, or JSON.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"format":"markdown","includeMedia":true,"includeStats":true,"maxTweets":50,"scrollDelay":1500,"autoDownload":true,"copyToClipboard":true},"stopGlobal":null},{"id":"video-downloader","title":"Video Downloader","emoji":"🎬","category":"scrape","danger":"safe","desc":"Download the video from any post, at your chosen quality.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"quality":"highest","autoDownload":true,"showAllQualities":true},"stopGlobal":null},{"id":"viral-tweets-scraper","title":"Viral Tweets Finder","emoji":"🔥","category":"scrape","danger":"safe","desc":"Find the top-performing viral posts from a search or any account.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"minLikes":50,"minRetweets":5,"minReplies":0,"maxTweets":100,"maxScrolls":50,"sortBy":"likes","scrollDelay":1500,"maxRetries":5,"exportJSON":true,"exportCSV":true},"stopGlobal":null},{"id":"monitor-account","title":"Account Monitor","emoji":"👀","category":"analyze","danger":"safe","desc":"Track follower/following changes on any public account.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"scrollDelay":2000,"maxScrolls":100,"maxRetries":5,"autoDownload":true},"stopGlobal":null},{"id":"audience-overlap","title":"Audience Overlap","emoji":"🔀","category":"analyze","danger":"safe","desc":"Compare two follower sets for overlap and unique handles.","where":{"label":"Any X page"},"defaults":{"listA":[],"listB":[],"labelA":"A","labelB":"B","storageKeyA":"","storageKeyB":"","printLimit":20,"exportResults":true},"stopGlobal":null},{"id":"best-time-to-post","title":"Best Time to Post","emoji":"⏰","category":"analyze","danger":"safe","desc":"Find when your audience is most active.","where":{"label":"Your own profile page"},"defaults":{"maxPosts":100,"scrollDelay":1500,"maxScrolls":50,"maxRetries":3},"stopGlobal":null},{"id":"competitor-analysis","title":"Competitor Analysis","emoji":"🕵️","category":"analyze","danger":"safe","desc":"Analyze a competitor account for content and engagement insights.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"maxPosts":50,"scrollDelay":1500,"maxScrolls":30,"maxRetries":3},"stopGlobal":null},{"id":"continuous-monitor","title":"Continuous Monitor","emoji":"🔄","category":"analyze","danger":"safe","desc":"Auto-refresh watch with browser notifications on follower changes.","where":{"label":"Your own profile page"},"defaults":{"checkIntervalMinutes":5,"enableNotifications":true,"enableSound":true,"scrollDelay":1500,"maxScrolls":50,"maxRetries":3},"stopGlobal":"stopMonitor"},{"id":"engagement-analytics","title":"Engagement Analytics","emoji":"📈","category":"analyze","danger":"safe","desc":"Break down likes, replies, and reposts across your posts.","where":{"label":"Your own profile page"},"defaults":{"maxPosts":50,"scrollDelay":1500,"maxScrolls":30,"maxRetries":3},"stopGlobal":null},{"id":"find-fake-followers","title":"Fake Follower Finder","emoji":"🤖","category":"analyze","danger":"safe","desc":"Identify likely bot/fake accounts in your audience.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"scrollDelay":1500,"maxScrolls":50,"maxRetries":3,"scoring":{"highFollowingRatio":25,"veryHighFollowingRatio":40,"defaultAvatar":20,"noBio":15,"suspiciousBio":25,"randomUsername":15,"massFollowing":15,"extremeFollowing":25,"noFollowers":20,"veryFewFollowers":10},"thresholds":{"highRatio":50,"veryHighRatio":100,"massFollowing":3000,"extremeFollowing":5000},"suspiciousKeywords":["crypto","nft","bitcoin","btc","eth","forex","trading signals","giveaway","airdrop","free money","passive income","onlyfans","fansly","dm for","link in bio","check bio","follow back","f4f","follow4follow","followback","18+","adult","nsfw","sexy","hot girl","make money","work from home","get rich","financial freedom","investment opportunity","guaranteed returns"],"minFakeScore":40,"likelyFakeScore":60},"stopGlobal":null},{"id":"audit-followers","title":"Follower Audit","emoji":"🔍","category":"analyze","danger":"safe","desc":"Score follower quality and surface likely fakes and inactives.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"scrollDelay":1500,"maxScrolls":100,"maxRetries":5,"thresholds":{"influencer":10,"balanced":0.5,"aggressive":0.1,"massFollower":2000,"selectiveFollower":100}},"stopGlobal":null},{"id":"followers-growth-tracker","title":"Growth Tracker","emoji":"📉","category":"analyze","danger":"safe","desc":"Track follower growth over time with saved history.","where":{"label":"Your own profile page"},"defaults":{"storageKey":"xactions_growth_tracker","maxHistory":365,"showChart":true},"stopGlobal":null},{"id":"hashtag-analytics","title":"Hashtag Analytics","emoji":"#️⃣","category":"analyze","danger":"safe","desc":"Measure how your hashtags perform.","where":{"label":"Your own profile page"},"defaults":{"maxPosts":100,"scrollDelay":1500,"maxScrolls":50,"maxRetries":3},"stopGlobal":null},{"id":"new-followers-alert","title":"New Follower Alerts","emoji":"🔔","category":"analyze","danger":"safe","desc":"Track new followers with optional welcome-message templates.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"scrollDelay":2000,"maxScrolls":100,"maxRetries":5,"welcomeMessages":["Hey {name}! Thanks for the follow! 🙏 Glad to connect!","Welcome {name}! 👋 Thanks for following! What brings you here?","Hey {name}! Appreciate the follow! Looking forward to connecting! 🚀","Thanks for following {name}! Always great to meet new people! ✨"],"autoDownload":true},"stopGlobal":null},{"id":"profile-stats","title":"Profile Stats","emoji":"📊","category":"analyze","danger":"safe","desc":"Get a quick, comprehensive stats card for any profile.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{},"stopGlobal":null},{"id":"sentiment-analyzer","title":"Sentiment Analyzer","emoji":"🧠","category":"analyze","danger":"safe","desc":"Score sentiment across posts on the current view.","where":{"label":"Any X page"},"defaults":{"maxPosts":40,"maxScrollAttempts":25,"scrollDelay":1600,"examples":3,"exportResults":true},"stopGlobal":"stopSentimentAnalyzer"},{"id":"shadowban-checker","title":"Shadowban Checker","emoji":"🚦","category":"analyze","danger":"safe","desc":"Heuristic search-suggestion / search-ban / reply-deboost check.","where":{"label":"Your own profile page"},"defaults":{"username":"auto","testDelay":2500,"exportResults":true},"stopGlobal":null},{"id":"trending-monitor","title":"Trending Monitor","emoji":"📈","category":"analyze","danger":"safe","desc":"Capture current trends and watch for your keywords.","where":{"label":"Explore / Trends","url":"https://x.com/explore","match":["^/explore"]},"defaults":{"watchKeywords":[],"repeatIntervalMs":0,"scrollRounds":4,"scrollDelay":1400,"exportResults":true},"stopGlobal":"stopTrendingMonitor"},{"id":"tweet-performance","title":"Tweet Performance","emoji":"📊","category":"analyze","danger":"safe","desc":"Rank your recent posts by engagement.","where":{"label":"Your own profile page"},"defaults":{"maxPosts":30,"sortBy":"likes","topN":10,"excludeRetweets":true,"maxScrollAttempts":40,"scrollDelay":1800,"exportResults":true},"stopGlobal":"stopTweetPerformance"},{"id":"detect-unfollowers","title":"Unfollower Detector","emoji":"💔","category":"analyze","danger":"safe","desc":"Compare against a saved snapshot to see who unfollowed you.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"scrollDelay":2000,"maxScrolls":100,"maxRetries":5,"autoDownload":true,"storageKey":"xactions_my_followers"},"stopGlobal":null},{"id":"follow-back","title":"Follow Back Everyone","emoji":"🔗","category":"grow","danger":"caution","desc":"Follow accounts that follow you but you don’t follow back.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"maxFollows":50,"skipVerified":false,"skipProtected":false,"minDelay":2000,"maxDelay":5000,"maxScrollRounds":40,"scrollDelay":2000,"maxEmptyScrolls":6},"stopGlobal":"stopFollowBack"},{"id":"follow-engagers","title":"Follow Engagers","emoji":"🧲","category":"grow","danger":"caution","desc":"Follow the people who liked or reposted a specific tweet.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"mode":"likers","maxFollows":20,"filters":{"minFollowers":100,"maxFollowers":50000,"skipProtected":true,"skipVerified":false},"minDelay":2000,"maxDelay":4000,"scrollDelay":1500},"stopGlobal":null},{"id":"follow-target-users","title":"Follow Target Audience","emoji":"🎯","category":"grow","danger":"caution","desc":"Follow the followers/following of accounts you specify.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"maxFollows":30,"maxScrolls":50,"filters":{"skipProtected":true,"skipVerified":false,"bioKeywords":[],"bioBlacklist":["bot","spam","promo"]},"minDelay":2000,"maxDelay":4000,"scrollDelay":1500},"stopGlobal":null},{"id":"growth-suite","title":"Growth Suite","emoji":"🚀","category":"grow","danger":"caution","desc":"All-in-one growth: auto-like, auto-follow, and smart-unfollow together.","where":{"label":"Any X page"},"defaults":{"keywords":["web3 developer","crypto trader","NFT artist"],"targetAccounts":[],"actions":{"follow":true,"like":true,"unfollow":true},"limits":{"follows":20,"likes":30,"unfollows":15},"timing":{"unfollowAfterDays":3,"delayBetweenActions":3000,"sessionDuration":30},"filters":{"minFollowers":50,"maxFollowers":50000,"mustHaveBio":true,"skipPrivate":true,"language":null}},"stopGlobal":null},{"id":"keyword-follow","title":"Keyword Follow","emoji":"🔑","category":"grow","danger":"caution","desc":"Follow users matching a keyword search, with bio filters.","where":{"label":"Search → People tab (x.com/search?...&f=user)","match":["^/search.*f=user","^/search"]},"defaults":{"maxFollows":20,"maxScrolls":30,"filters":{"skipProtected":true,"skipMutuals":false,"skipVerified":false,"bioMustContain":[],"bioBlacklist":["bot","automated","promo","giveaway"]},"trackFollows":true,"minDelay":2000,"maxDelay":5000,"scrollDelay":2000},"stopGlobal":null},{"id":"auto-commenter","title":"Auto Commenter","emoji":"💬","category":"engage","danger":"caution","desc":"Comment on a target user's posts with your templates.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"comments":["🔥","Great point!","This is so true 👏","Interesting perspective!","Thanks for sharing this 🙏","💯","Well said!","Couldn't agree more","👀 interesting","This is gold ✨"],"maxComments":5,"maxPostAgeMinutes":60,"minPostAgeSeconds":30,"onlyOriginalTweets":true,"onlyWithMedia":false,"minDelay":30000,"maxDelay":60000,"scrollDelay":2000},"stopGlobal":null},{"id":"auto-liker","title":"Auto Liker","emoji":"❤️","category":"engage","danger":"caution","desc":"Like posts in a timeline or on a profile, at a human pace.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"likeAll":false,"keywords":["web3","crypto","AI","startup"],"fromUsers":[],"maxLikes":20,"maxScrolls":50,"alsoRetweet":false,"skipReplies":true,"skipAds":true,"minDelay":2000,"maxDelay":5000,"scrollDelay":2000},"stopGlobal":null},{"id":"auto-repost","title":"Auto Repost","emoji":"🔁","category":"engage","danger":"caution","desc":"Repost posts matching your criteria as you scroll.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"maxReposts":10,"skipAds":true,"skipReplies":true,"onlyWithMedia":false,"mustContainKeywords":[],"minDelay":1500,"maxDelay":4000,"maxScrollAttempts":25,"noNewPostsThreshold":5},"stopGlobal":"stopAutoRepost"},{"id":"auto-reply-mentions","title":"Auto-Reply Mentions","emoji":"📨","category":"engage","danger":"caution","desc":"Reply to your recent mentions with rotating templates.","where":{"label":"Your Mentions tab","url":"https://x.com/notifications/mentions","match":["^/notifications/mentions"]},"defaults":{"replyTemplates":["Thanks for the mention! 🙏","Appreciate you tagging me 🚀","Thanks for reaching out!","Glad you brought this up 💡"],"maxReplies":8,"skipIfAlreadyReplied":true,"addRandomEmoji":false,"minDelay":3000,"maxDelay":6000,"maxScrollAttempts":20,"noNewMentionsThreshold":4},"stopGlobal":"stopAutoReplyMentions"},{"id":"comment-by-hashtag","title":"Comment by Hashtag","emoji":"🗨️","category":"engage","danger":"caution","desc":"Find hashtag posts and comment with your templates.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"hashtags":["web3","crypto","NFT"],"comments":["Great point! 🔥","This is so true! 💯","Interesting perspective!","Thanks for sharing this! 🙌","Couldn't agree more!"],"maxComments":10,"minDelay":3000,"maxDelay":6000,"skipUsernames":[],"minLikes":0,"minRetweets":0},"stopGlobal":null},{"id":"comment-by-location","title":"Comment by Location","emoji":"📍","category":"engage","danger":"caution","desc":"Find posts from a location and comment on them.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"location":"New York","geocode":null,"searchQuery":"","comments":["Love seeing posts from this area! 🌍","Great content from a great place! 🔥","Thanks for sharing! 💯","Awesome post! 🙌","This is amazing! ✨"],"maxComments":10,"minDelay":3000,"maxDelay":7000,"skipUsernames":[],"maxTweetAge":24,"skipRetweets":true},"stopGlobal":null},{"id":"interact-by-hashtag","title":"Interact by Hashtag","emoji":"#️⃣","category":"engage","danger":"caution","desc":"Like/follow/reply on posts matching a hashtag.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"hashtags":["crypto","web3","bitcoin"],"actions":{"like":true,"retweet":false,"follow":true},"limits":{"likes":20,"retweets":5,"follows":10,"tweetsPerHashtag":10},"filters":{"minLikes":5,"minRetweets":0,"skipReplies":true,"skipRetweets":true,"requireMedia":false},"delayBetweenActions":2000,"scrollDelay":2000},"stopGlobal":null},{"id":"interact-by-place","title":"Interact by Place","emoji":"📍","category":"engage","danger":"caution","desc":"Like/follow/reply on posts from a location.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"locations":[{"name":"New York","query":"near:\"New York\""},{"name":"San Francisco","query":"near:\"San Francisco\""}],"keywords":[],"actions":{"like":true,"follow":true,"retweet":false},"limits":{"likes":15,"follows":10,"retweets":3},"delayBetweenActions":2000,"scrollDelay":2000},"stopGlobal":null},{"id":"interact-with-likers","title":"Interact with Likers","emoji":"🧲","category":"engage","danger":"caution","desc":"Engage the users who liked a specific post.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"actions":{"follow":true},"limits":{"follows":20},"filters":{"skipPrivate":true,"skipVerified":false,"skipNoPhoto":false},"delayBetweenActions":2000,"scrollDelay":2000},"stopGlobal":null},{"id":"interact-by-users","title":"Interact with Users","emoji":"🎯","category":"engage","danger":"caution","desc":"Full like/follow/reply suite aimed at specific users.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"targetUsers":[],"actions":{"like":true,"retweet":false,"reply":false,"follow":true},"limits":{"likesPerUser":3,"retweetsPerUser":1,"repliesPerUser":1},"delayBetweenActions":2000,"delayBetweenUsers":5000,"replyTemplates":["Great point! 🔥","Couldn't agree more 👏","This is gold 💯","Thanks for sharing!"]},"stopGlobal":null},{"id":"like-by-user","title":"Like a User","emoji":"👤","category":"engage","danger":"caution","desc":"Auto-like posts from a specific user's profile.","where":{"label":"The profile you want to target (x.com/username)"},"defaults":{"maxLikes":50,"skipReplies":false,"skipRetweets":true,"skipQuoteTweets":false,"onlyWithMedia":false,"minLikes":0,"minRetweets":0,"minDelay":1500,"maxDelay":3500,"maxScrollAttempts":25,"stopAfterAlreadyLiked":10},"stopGlobal":"stopLikeByUser"},{"id":"like-by-hashtag","title":"Like by Hashtag","emoji":"#️⃣","category":"engage","danger":"caution","desc":"Auto-like posts containing specific hashtags.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"hashtags":["javascript","webdev","coding"],"maxLikesPerHashtag":10,"maxTotalLikes":30,"minDelay":2000,"maxDelay":4000,"skipRetweets":true,"skipMediaOnly":false,"maxScrollAttempts":5},"stopGlobal":"stopLikeByHashtag"},{"id":"like-by-location","title":"Like by Location","emoji":"📍","category":"engage","danger":"caution","desc":"Auto-like posts from a geographic area.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"location":"San Francisco","radiusMiles":25,"keyword":"","maxLikes":30,"minDelay":2000,"maxDelay":4000,"skipRetweets":true,"skipReplies":false,"maxScrollAttempts":15,"searchType":"live"},"stopGlobal":"stopLikeByLocation"},{"id":"like-by-feed","title":"Like Home Feed","emoji":"🏠","category":"engage","danger":"caution","desc":"Auto-like posts as you scroll your home timeline.","where":{"label":"Your Home timeline","url":"https://x.com/home","match":["^/home"]},"defaults":{"maxLikes":50,"skipReplies":true,"skipAds":true,"skipRetweets":true,"onlyWithMedia":false,"minDelay":1500,"maxDelay":3500,"maxScrollAttempts":20,"noNewTweetsThreshold":5},"stopGlobal":"stopLikeByFeed"},{"id":"like-user-replies","title":"Like Replies","emoji":"↩️","category":"engage","danger":"caution","desc":"Auto-like the replies under a specific post.","where":{"label":"The open tweet/thread (its status page)","match":["/status/"]},"defaults":{"maxLikes":30,"skipNestedReplies":false,"onlyVerified":false,"onlyWithMedia":false,"skipContaining":[],"onlyContaining":[],"minDelay":1500,"maxDelay":3500,"maxScrollAttempts":20,"skipOriginalTweet":true},"stopGlobal":"stopLikeUserReplies"},{"id":"vote-in-polls","title":"Vote in Polls","emoji":"🗳️","category":"engage","danger":"caution","desc":"Auto-vote a chosen option on polls in the timeline.","where":{"label":"Search → Latest tab (x.com/search?...&f=live)","match":["^/search"]},"defaults":{"strategy":"random","optionIndex":0,"maxVotes":15,"minDelay":1500,"maxDelay":4000,"maxScrollAttempts":30,"noNewPollsThreshold":6},"stopGlobal":"stopVoteInPolls"},{"id":"delete-tweets","title":"Bulk Delete Your Posts","emoji":"🗑️","category":"cleanup","danger":"destructive","desc":"Delete your own tweets by age, keyword, or engagement (dry-run by default).","where":{"label":"Your own profile page"},"defaults":{"dryRun":false,"olderThanDays":0,"containingKeywords":[],"minLikesToKeep":0,"maxDeletes":25,"minDelay":3000,"maxDelay":7000,"scrollDelay":4000,"menuDelay":1200,"countdownSeconds":5,"maxScrollAttempts":60,"noNewPostsThreshold":5},"stopGlobal":"stopDeleteTweets"},{"id":"clear-all-bookmarks","title":"Clear All Bookmarks","emoji":"🔖","category":"cleanup","danger":"destructive","desc":"Remove all of your bookmarks.","where":{"label":"Your Bookmarks","url":"https://x.com/i/bookmarks","match":["^/i/bookmarks"]},"defaults":{"maxRemove":0,"removeDelay":1500,"scrollDelay":2000,"maxRetries":5,"confirmStart":true},"stopGlobal":null},{"id":"clear-all-likes","title":"Clear All Likes","emoji":"🗑️","category":"cleanup","danger":"destructive","desc":"Remove all likes from your account.","where":{"label":"Your Likes page (x.com/<you>/likes)","match":["/likes"]},"defaults":{"maxUnlikes":0,"unlikeDelay":1500,"scrollDelay":2000,"maxRetries":5,"confirmStart":true},"stopGlobal":null},{"id":"clear-all-retweets","title":"Clear All Reposts","emoji":"🔁","category":"cleanup","danger":"destructive","desc":"Undo all of your reposts.","where":{"label":"Your own profile page"},"defaults":{"maxUndo":0,"unretweetDelay":2000,"scrollDelay":2500,"maxRetries":5,"confirmStart":true},"stopGlobal":null},{"id":"smart-unfollow","title":"Smart Unfollow","emoji":"🧠","category":"cleanup","danger":"destructive","desc":"Unfollow accounts that didn't follow back within N days (respects your whitelist).","where":{"label":"Your Following page (x.com/<you>/following)","match":["/following"]},"defaults":{"daysToWait":3,"maxUnfollows":30,"whitelist":[],"onlyTracked":true,"dryRun":false,"unfollowDelay":1500,"confirmDelay":1000,"scrollDelay":2000,"maxScrolls":100,"maxRetries":5},"stopGlobal":null},{"id":"unfollow-with-log","title":"Unfollow + Log","emoji":"📝","category":"cleanup","danger":"destructive","desc":"Unfollow non-followers and download a log of who was removed.","where":{"label":"Your Following page (x.com/<you>/following)","match":["/following"]},"defaults":{"maxRetries":5,"unfollowDelay":1500,"confirmDelay":1000,"scrollDelay":2000,"maxUnfollows":0,"autoDownload":true,"includeTimestamp":true},"stopGlobal":"stopUnfollow"},{"id":"unfollow-everyone","title":"Unfollow Everyone","emoji":"🧹","category":"cleanup","danger":"destructive","desc":"Mass-unfollow every account you follow.","where":{"label":"Your Following page (x.com/<you>/following)","match":["/following"]},"defaults":{"maxRetries":5,"unfollowDelay":1500,"confirmDelay":1000,"scrollDelay":2000,"maxUnfollows":0,"confirmStart":true},"stopGlobal":"stopUnfollow"},{"id":"unfollow-non-followers","title":"Unfollow Non-Followers","emoji":"✂️","category":"cleanup","danger":"destructive","desc":"Unfollow accounts that don't follow you back.","where":{"label":"Your Following page (x.com/<you>/following)","match":["/following"]},"defaults":{"maxRetries":5,"unfollowDelay":1500,"confirmDelay":1000,"scrollDelay":2000,"maxUnfollows":0,"confirmStart":true,"logKept":true},"stopGlobal":"stopUnfollow"},{"id":"unlike-all","title":"Unlike Everything","emoji":"💔","category":"cleanup","danger":"destructive","desc":"Remove every like from your Likes page.","where":{"label":"Your Likes page (x.com/<you>/likes)","match":["/likes"]},"defaults":{"maxUnlikes":1000,"minDelay":1000,"maxDelay":2500,"scrollDelay":1500,"confirmBeforeStart":true,"maxScrollAttempts":5,"logProgress":true},"stopGlobal":"stopUnlike"},{"id":"unlike-old","title":"Unlike Old Likes","emoji":"🕰️","category":"cleanup","danger":"destructive","desc":"Remove likes older than a number of days you set.","where":{"label":"Your Likes page (x.com/<you>/likes)","match":["/likes"]},"defaults":{"daysOld":30,"maxUnlikes":500,"minDelay":1000,"maxDelay":2500,"scrollDelay":1500,"maxScrollAttempts":10,"logProgress":true},"stopGlobal":"stopUnlike"},{"id":"block-bots","title":"Block Bots","emoji":"🤖","category":"moderate","danger":"destructive","desc":"Detect and block likely bots by ratio, age, and bio.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"scrollDelay":1500,"maxScrolls":50,"maxRetries":3,"blockDelay":2000,"detection":{"maxFollowingRatio":50,"minAccountAgeDays":30,"maxFollowing":5000,"minFollowers":5,"suspiciousBioKeywords":["crypto","nft","giveaway","airdrop","free money","onlyfans","dm for","follow back","f4f","follow4follow","bitcoin","eth","$btc","$eth","forex","trading signals","make money","passive income","work from home","link in bio","check bio","clickhere","sexo","sex","camgirl","hot girl","sugar","seeking arrangement"],"flagDefaultAvatar":true,"flagNoBio":true,"flagRandomUsername":true},"dryRun":true,"maxBlocks":50},"stopGlobal":null},{"id":"block-by-keywords","title":"Block by Keywords","emoji":"🚫","category":"moderate","danger":"destructive","desc":"Block users whose bio contains keywords you set.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"blockKeywords":["crypto","nft","giveaway","airdrop","onlyfans","dm for promo","follow back","f4f"],"scrollDelay":1500,"maxScrolls":30,"maxRetries":3,"blockDelay":2000,"dryRun":true,"maxBlocks":50},"stopGlobal":null},{"id":"block-by-ratio","title":"Block by Ratio","emoji":"📛","category":"moderate","danger":"destructive","desc":"Block accounts by follower/following ratio.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"maxRatio":50,"minFollowing":100,"minFollowers":5,"scrollDelay":2000,"maxScrolls":30,"maxRetries":3,"blockDelay":2000,"dryRun":true,"maxBlocks":30},"stopGlobal":null},{"id":"block-list-transfer","title":"Block List Import/Export","emoji":"🧱","category":"moderate","danger":"destructive","desc":"Export your block list, import-and-block a list, or block an account’s followers.","where":{"label":"Blocked accounts","url":"https://x.com/settings/blocked_all","match":["^/settings/blocked"]},"defaults":{"mode":"export","usernames":[],"targetAccount":"","whitelist":[],"maxBlocks":25,"dryRun":true,"minDelay":3000,"maxDelay":6000,"maxScrollRounds":40,"scrollDelay":2000,"maxEmptyScrolls":6},"stopGlobal":"stopBlockListTransfer"},{"id":"mass-block","title":"Mass Block","emoji":"⛔","category":"moderate","danger":"destructive","desc":"Block every user in a list you provide.","where":{"label":"Any X page"},"defaults":{"usersToBlock":[],"blockDelay":3000,"dryRun":true},"stopGlobal":"stopMassBlock"},{"id":"mass-unblock","title":"Mass Unblock","emoji":"✅","category":"moderate","danger":"destructive","desc":"Unblock accounts in bulk from your blocked list.","where":{"label":"Blocked accounts","url":"https://x.com/settings/blocked_all","match":["^/settings/blocked"]},"defaults":{"unblockAll":true,"usersToUnblock":[],"unblockDelay":1500,"maxUnblocks":100,"scrollDelay":1500,"maxScrolls":20,"dryRun":true},"stopGlobal":"stopMassUnblock"},{"id":"mass-unmute","title":"Mass Unmute","emoji":"🔊","category":"moderate","danger":"destructive","desc":"Unmute accounts in bulk from your muted list.","where":{"label":"Muted accounts","url":"https://x.com/settings/muted_all","match":["^/settings/muted"]},"defaults":{"unmuteAll":true,"usersToUnmute":[],"unmuteDelay":1000,"maxUnmutes":200,"scrollDelay":1500,"maxScrolls":30,"dryRun":true},"stopGlobal":"stopMassUnmute"},{"id":"mute-by-keywords","title":"Mute by Keywords","emoji":"🔇","category":"moderate","danger":"destructive","desc":"Mute users whose bio contains keywords you set.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"muteKeywords":["crypto","nft","giveaway","trading signals","dm for promo"],"scrollDelay":1500,"maxScrolls":30,"maxRetries":3,"muteDelay":2000,"dryRun":true,"maxMutes":50},"stopGlobal":"stopMuteByKeywords"},{"id":"manage-muted-words","title":"Muted Words","emoji":"🔇","category":"moderate","danger":"caution","desc":"Add, remove, or list your muted words and phrases.","where":{"label":"Muted words settings","url":"https://x.com/settings/muted_keywords","match":["muted_keywords"]},"defaults":{"action":"list","words":["giveaway","airdrop scam"],"muteOptions":{"from":"anyone","home":true,"notifications":true,"duration":"forever"},"minDelay":1500,"maxDelay":4000},"stopGlobal":"stopManageMutedWords"},{"id":"remove-follower","title":"Remove a Follower","emoji":"🚪","category":"moderate","danger":"caution","desc":"Remove followers without blocking (dry-run by default).","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"usernames":[],"whitelist":[],"maxRemove":25,"dryRun":true,"confirm":true,"minDelay":2500,"maxDelay":5000,"maxScrollRounds":40,"scrollDelay":2000,"maxEmptyScrolls":6},"stopGlobal":"stopRemoveFollower"},{"id":"report-spam","title":"Report Spam","emoji":"🚩","category":"moderate","danger":"destructive","desc":"Report spam accounts from your followers or mentions.","where":{"label":"Your Followers page (x.com/<you>/followers)","match":["/followers"]},"defaults":{"spamKeywords":["free giveaway","click my link","dm for cashapp","send nudes","sexchat","hot girls in","make $1000 daily","guaranteed profits"],"detection":{"flagDefaultAvatar":true,"maxFollowingRatio":100,"flagNewAccounts":true,"flagExternalLinks":false},"scrollDelay":1500,"maxScrolls":20,"dryRun":true,"maxReports":10,"reportDelay":5000},"stopGlobal":null},{"id":"add-to-list","title":"Add Users to List","emoji":"➕","category":"lists","danger":"caution","desc":"Add specified users to one of your Lists.","where":{"label":"Your Lists","url":"https://x.com/i/lists","match":["/lists"]},"defaults":{"listName":"My List","usernames":[],"dryRun":true,"minDelay":2000,"maxDelay":4000,"searchWait":2200,"stepDelay":1500},"stopGlobal":"stopAddToList"},{"id":"follow-list-members","title":"Follow List Members","emoji":"👣","category":"lists","danger":"caution","desc":"Follow every member of a List.","where":{"label":"A List's members page","match":["/lists/.*members","/members"]},"defaults":{"maxFollows":50,"maxScrollAttempts":100,"noNewThreshold":5,"skipVerified":false,"skipProtected":false,"minDelay":2000,"maxDelay":5000,"scrollDelay":2000},"stopGlobal":"stopFollowListMembers"},{"id":"list-manager","title":"List Manager","emoji":"🗂️","category":"lists","danger":"caution","desc":"Create, rename, or delete a List.","where":{"label":"Your Lists","url":"https://x.com/i/lists","match":["/lists"]},"defaults":{"action":"create","name":"My New List","newName":"","description":"Created with XActions","isPrivate":false,"dryRun":true,"stepDelay":1500},"stopGlobal":"stopListManager"},{"id":"join-communities","title":"Join Communities","emoji":"➕","category":"community","danger":"caution","desc":"Join multiple Communities from a list of IDs.","where":{"label":"Any X page"},"defaults":{"communities":[],"joinDelay":3000,"navigationDelay":3000,"maxJoin":0,"skipAlreadyJoined":true},"stopGlobal":null},{"id":"leave-community","title":"Leave a Community","emoji":"➖","category":"community","danger":"caution","desc":"Leave one Community by name or ID.","where":{"label":"Your Communities","url":"https://x.com/i/communities","match":["communities"]},"defaults":{"communityId":null,"confirmDelay":1500,"navigationDelay":2500},"stopGlobal":null},{"id":"leave-all-communities","title":"Leave All Communities","emoji":"🚪","category":"community","danger":"destructive","desc":"Leave every Community you have joined.","where":{"label":"Your Communities","url":"https://x.com/i/communities","match":["communities"]},"defaults":{"leaveDelay":1500,"confirmDelay":2000,"navDelay":2500,"maxToLeave":0},"stopGlobal":"stopLeaveCommunities"},{"id":"auto-reply-dms","title":"Auto-Reply DMs","emoji":"💌","category":"profile","danger":"caution","desc":"Auto-reply to unread DM conversations with a template.","where":{"label":"Your Messages","url":"https://x.com/messages","match":["^/messages"]},"defaults":{"message":"Thanks for reaching out! I'll get back to you soon. 🙏","maxReplies":15,"onlyUnread":true,"minDelay":4000,"maxDelay":9000,"maxScrollRounds":12,"scrollDelay":1500},"stopGlobal":"stopAutoReplyDMs"},{"id":"bulk-dm","title":"Bulk / Welcome DM","emoji":"📤","category":"profile","danger":"caution","desc":"DM a list of users with a personalized template.","where":{"label":"Your Messages","url":"https://x.com/messages","match":["^/messages"]},"defaults":{"usernames":[],"message":"Hey {name}! 👋 Thanks for connecting.","maxDMs":10,"skipExisting":true,"minDelay":25000,"maxDelay":45000},"stopGlobal":"stopBulkDM"},{"id":"edit-profile","title":"Edit Profile","emoji":"🪪","category":"profile","danger":"caution","desc":"Update your name, bio, location, website, or birthday.","where":{"label":"Edit profile","url":"https://x.com/settings/profile","match":["^/settings/profile"]},"defaults":{"name":null,"bio":null,"location":null,"website":null,"birthdate":null,"autoSave":true,"actionDelay":700},"stopGlobal":null},{"id":"multi-account","title":"Multi-Account Manager","emoji":"🔀","category":"profile","danger":"safe","desc":"Manage and switch between multiple accounts.","where":{"label":"Any X page"},"defaults":{"storagePrefix":"xactions_multi_","autoDetect":true},"stopGlobal":null},{"id":"account-settings","title":"Privacy & Settings","emoji":"⚙️","category":"profile","danger":"caution","desc":"Toggle common privacy and safety settings.","where":{"label":"Settings","url":"https://x.com/settings","match":["^/settings"]},"defaults":{"settings":{},"actionDelay":900},"stopGlobal":null},{"id":"send-direct-message","title":"Send DMs","emoji":"✉️","category":"profile","danger":"caution","desc":"Send direct messages, with per-recipient personalization.","where":{"label":"Your Messages","url":"https://x.com/messages","match":["^/messages"]},"defaults":{"targetUsers":[],"messageTemplate":"Hey {username}! 👋\n\nJust wanted to reach out and connect.\n\nBest,\n[Your Name]","limits":{"messagesPerSession":10,"delayBetweenMessages":30000},"options":{"skipIfConversationExists":true,"randomizeDelay":true}},"stopGlobal":null},{"id":"update-profile-picture","title":"Update Avatar","emoji":"🖼️","category":"profile","danger":"caution","desc":"Guided helper for changing your profile picture.","where":{"label":"Edit profile","url":"https://x.com/settings/profile","match":["^/settings/profile"]},"defaults":{"actionDelay":1500,"autoOpenPicker":true},"stopGlobal":null},{"id":"update-banner","title":"Update Banner","emoji":"🖼️","category":"profile","danger":"caution","desc":"Guided helper for changing your profile banner.","where":{"label":"Edit profile","url":"https://x.com/settings/profile","match":["^/settings/profile"]},"defaults":{"actionDelay":1500,"autoOpenEditor":true},"stopGlobal":null},{"id":"update-bio","title":"Update Bio","emoji":"✍️","category":"profile","danger":"caution","desc":"Update your profile bio.","where":{"label":"Edit profile","url":"https://x.com/settings/profile","match":["^/settings/profile"]},"defaults":{"newBio":"🚀 Building cool stuff with code\n🐦 Automating X with @XActions\n💡 Open source enthusiast\n🔗 github.com/nirholas/XActions","actionDelay":1000,"autoSave":true},"stopGlobal":null},{"id":"blacklist","title":"Blacklist Manager","emoji":"📕","category":"utility","danger":"safe","desc":"Maintain a list of users other tools should skip.","where":{"label":"Any X page"},"defaults":{"storageKey":"xactions_blacklist","defaultBlacklist":[]},"stopGlobal":null},{"id":"filter-manager","title":"Filter Manager","emoji":"🎚️","category":"utility","danger":"safe","desc":"Configure shared filters used across the automation tools.","where":{"label":"Any X page"},"defaults":{},"stopGlobal":null},{"id":"notification-manager","title":"Notification Cleaner","emoji":"🔕","category":"utility","danger":"safe","desc":"Mark notifications read and summarize them.","where":{"label":"Your Notifications","url":"https://x.com/notifications","match":["^/notifications"]},"defaults":{"action":"summary","maxNotifications":100,"exportResults":true,"maxScrollRounds":30,"scrollDelay":1400},"stopGlobal":null},{"id":"protect-active-users","title":"Protect Active Users","emoji":"🛡️","category":"utility","danger":"safe","desc":"Scan your posts for engaged users and protect them from unfollow.","where":{"label":"Your own profile page"},"defaults":{"postsToScan":10,"engagementTypes":{"likers":true,"repliers":true,"retweeters":true,"quoters":false},"lookbackDays":30,"minEngagements":1,"scrollDelay":1500,"maxScrollsPerList":10},"stopGlobal":null},{"id":"rate-limiter","title":"Rate Limiter","emoji":"⏱️","category":"utility","danger":"safe","desc":"Tune the pacing/quota helper shared by the action tools.","where":{"label":"Any X page"},"defaults":{},"stopGlobal":null},{"id":"whitelist","title":"Whitelist Manager","emoji":"📗","category":"utility","danger":"safe","desc":"Maintain a list of users to protect from actions.","where":{"label":"Any X page"},"defaults":{"storageKey":"xactions_whitelist","defaultWhitelist":[]},"stopGlobal":null}];
const CATEGORIES = [{"id":"create","label":"Create & Post","emoji":"✍️"},{"id":"scrape","label":"Scrape & Export","emoji":"📥"},{"id":"analyze","label":"Analytics","emoji":"📊"},{"id":"grow","label":"Grow","emoji":"🌱"},{"id":"engage","label":"Engage","emoji":"💬"},{"id":"cleanup","label":"Clean Up","emoji":"🧹"},{"id":"moderate","label":"Moderate","emoji":"🛡️"},{"id":"lists","label":"Lists","emoji":"🗂️"},{"id":"community","label":"Communities","emoji":"👥"},{"id":"profile","label":"Profile","emoji":"🪪"},{"id":"utility","label":"Utilities","emoji":"🧰"}];
const TOOLS = {};
function register(id, fn){ TOOLS[id] = fn; }
register("account-settings", function(){
(async function accountSettings() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
// Only include the keys you want to change. Any key you omit is left untouched.
const CONFIG = {
settings: {
// true = protect your posts (private account), false = public.
// protectPosts: true,
// true = allow others to tag you in photos, false = turn photo tagging off.
// photoTagging: false,
// true = people with your email can find you, false = they cannot.
// discoverableByEmail: false,
// Who can send you message requests: 'everyone' | 'verified' | 'no-one'.
// allowDMsFrom: 'verified',
},
// Delay for UI interactions (ms).
actionDelay: 900
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS + SETTINGS MAP
// ============================================
const SELECTORS = {
confirm: '[data-testid="confirmationSheetConfirm"]'
};
// Each supported setting: which /settings page it lives on, its type, and how
// to find its control. testids are tried first, then a label-text fallback,
// because X rotates and renames testids across builds.
const SETTINGS = {
protectPosts: {
page: '/settings/audience_and_tagging',
type: 'switch',
testids: ['[data-testid="protectedTweets"]'],
labelNeedles: ['protect your posts', 'protect your tweets'],
label: 'Protect your posts'
},
photoTagging: {
page: '/settings/audience_and_tagging',
type: 'switch',
testids: ['[data-testid="allowTagging"]', '[data-testid="photoTagging"]'],
labelNeedles: ['photo tagging', 'tag you in photos', 'allow people to tag'],
label: 'Photo tagging'
},
discoverableByEmail: {
page: '/settings/discoverability_and_contacts',
type: 'switch',
testids: ['[data-testid="allowEmailReverseLookup"]', '[data-testid="discoverableByEmail"]'],
labelNeedles: ['email address find', 'your email address'],
label: 'Discoverable by email'
},
allowDMsFrom: {
page: '/settings/messages',
type: 'radio',
label: 'Allow message requests from',
// desired value -> label text needles for the matching radio option.
options: {
everyone: ['everyone', 'no filtering', 'anyone'],
verified: ['verified'],
'no-one': ['no one', 'no-one', 'nobody', 'off']
}
}
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const findByTestids = (testids) => {
for (const t of testids || []) {
const el = document.querySelector(t);
if (el) return el;
}
return null;
};
// Find a role=switch by nearby label text when no testid matches.
const findSwitchByLabel = (needles) => {
const wants = needles.map(n => n.toLowerCase());
const switches = document.querySelectorAll('[role="switch"]');
for (const sw of switches) {
const scope = sw.closest('label') || sw.closest('[role="group"]') || sw.parentElement?.parentElement || sw.parentElement;
const txt = (scope?.textContent || '').toLowerCase();
if (wants.some(w => txt.includes(w))) return sw;
}
return null;
};
const findRadioByText = (needles) => {
const wants = needles.map(n => n.toLowerCase());
const radios = document.querySelectorAll('[role="radio"], label');
for (const r of radios) {
const txt = (r.textContent || '').trim().toLowerCase();
if (txt && wants.some(w => txt.includes(w))) return r;
}
return null;
};
// SPA navigation: if we are not on `path`, click a same-page settings link to
// it (no full reload, so this script keeps running). Returns true when there.
const ensureOnPage = async (path) => {
if (window.location.pathname === path) return true;
const link = document.querySelector(`a[href="${path}"]`);
if (link) {
link.click();
for (let i = 0; i < 12; i++) {
await sleep(400);
if (window.location.pathname === path) return true;
}
}
return window.location.pathname === path;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { changed: 0, alreadyCorrect: 0, notFound: 0, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ ⚙️ ACCOUNT SETTINGS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn, do not hard-redirect.
if (!/^\/settings/.test(window.location.pathname)) {
log.warning('Go to x.com/settings (any /settings/* page), then run this again.');
return stats;
}
const requested = Object.keys(CONFIG.settings || {}).filter(k => CONFIG.settings[k] !== undefined);
if (requested.length === 0) {
log.warning('No settings requested. Uncomment and set at least one key inside CONFIG.settings.');
return stats;
}
log.info(`Settings to apply: ${requested.join(', ')}`);
const applySwitch = async (def, desired) => {
let sw = findByTestids(def.testids);
if (!sw) sw = findSwitchByLabel(def.labelNeedles || []);
if (!sw) {
log.warning(`${def.label}: toggle not found on ${def.page}. Skipped (X may have renamed it, or the section did not render).`);
stats.notFound++;
return;
}
const current = sw.getAttribute('aria-checked') === 'true';
if (current === !!desired) {
log.info(`${def.label}: already ${desired ? 'ON' : 'OFF'}. No change.`);
stats.alreadyCorrect++;
return;
}
sw.click();
await sleep(CONFIG.actionDelay);
// Protect-posts style changes can raise a confirmation sheet.
const confirm = document.querySelector(SELECTORS.confirm);
if (confirm) {
confirm.click();
await sleep(CONFIG.actionDelay);
}
log.success(`${def.label}: set to ${desired ? 'ON' : 'OFF'}.`);
stats.changed++;
};
const applyRadio = async (def, desired) => {
const key = String(desired).toLowerCase();
const needles = def.options[key];
if (!needles) {
log.error(`${def.label}: "${desired}" is not a valid value. Use one of: ${Object.keys(def.options).join(', ')}.`);
stats.errors++;
return;
}
const opt = findRadioByText(needles);
if (!opt) {
log.warning(`${def.label}: option "${desired}" not found on ${def.page}. Skipped.`);
stats.notFound++;
return;
}
const radio = opt.getAttribute('role') === 'radio' ? opt : opt.querySelector('[role="radio"]');
if (radio && radio.getAttribute('aria-checked') === 'true') {
log.info(`${def.label}: already "${desired}". No change.`);
stats.alreadyCorrect++;
return;
}
opt.click();
await sleep(CONFIG.actionDelay);
log.success(`${def.label}: set to "${desired}".`);
stats.changed++;
};
for (const key of requested) {
const def = SETTINGS[key];
if (!def) {
log.warning(`Unknown setting "${key}". Supported: ${Object.keys(SETTINGS).join(', ')}. Skipped.`);
stats.notFound++;
continue;
}
const desired = CONFIG.settings[key];
try {
const onPage = await ensureOnPage(def.page);
if (!onPage) {
log.warning(`${def.label}: could not navigate to ${def.page} from here. Open x.com${def.page} and run this again to apply it.`);
stats.notFound++;
continue;
}
await sleep(CONFIG.actionDelay);
if (def.type === 'switch') await applySwitch(def, desired);
else if (def.type === 'radio') await applyRadio(def, desired);
} catch (error) {
log.error(`${def.label}: ${error.message}`);
stats.errors++;
}
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 ACCOUNT SETTINGS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Changed: ${String(stats.changed).padEnd(32)}║
║ ➖ Already correct: ${String(stats.alreadyCorrect).padEnd(32)}║
║ ⚠️ Not found: ${String(stats.notFound).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("add-to-list", function(){
(async function addToList() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// The EXISTING list to add people to (must appear on your Lists page)
listName: 'My List',
// Users to add (handles without the leading @)
usernames: [
// 'user1',
// 'user2'
],
// Safety: preview without adding anyone. Set to false to execute.
dryRun: true,
// Rate limiting between users (randomized between min and max, ms)
minDelay: 2000,
maxDelay: 4000,
// How long to wait for search results per user (ms)
searchWait: 2200,
// Delay between UI navigation steps (ms)
stepDelay: 1500
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
listLink: 'a[href*="/lists/"]',
editButton: '[data-testid="editList"], [aria-label="Edit List"], [aria-label="Edit list"]',
manageMembers: '[data-testid="listManageMembers"]',
addMembersTab: '[data-testid="addMembers"]',
searchInput: '[data-testid="searchPeople"], input[data-testid="searchPeople"]',
userCell: '[data-testid="UserCell"]',
// Inside the add-members list, each row exposes an add/remove follow-style button
addButton: '[data-testid$="-follow"], [role="button"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
step: (msg) => console.log(` → ${msg}`)
};
let stopped = false;
window.stopAddToList = () => {
stopped = true;
log.warning('Stop requested. Finishing the current user, then exiting.');
};
const qs = (sel) => document.querySelector(sel);
const waitFor = async (sel, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (stopped) return null;
const el = qs(sel);
if (el) return el;
await sleep(200);
}
return null;
};
const setReactInput = (el, value) => {
const proto = el.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
const findButtonByText = (labels) => {
const wanted = labels.map(l => l.toLowerCase());
for (const b of document.querySelectorAll('[role="button"], button, [role="tab"], [role="menuitem"]')) {
const t = (b.textContent || '').trim().toLowerCase();
if (wanted.some(w => t === w || t.startsWith(w))) return b;
}
return null;
};
const findListLinkByName = (name) => {
const target = name.trim().toLowerCase();
for (const link of document.querySelectorAll(SELECTORS.listLink)) {
const text = (link.textContent || '').trim().toLowerCase();
if (text.includes(target)) return link;
}
return null;
};
const handleFromCell = (cell) => {
const link = cell.querySelector('a[href^="/"]');
if (!link) return '';
const href = link.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
return m ? m[1] : '';
};
// ============================================
// 🚀 RUN
// ============================================
const stats = { added: 0, notFound: 0, alreadyIn: 0, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ ➕ ADD TO LIST - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard
if (!/\/lists/.test(window.location.pathname) && !/\/i\/lists/.test(window.location.pathname)) {
log.warning('You are not on a Lists page.');
log.info('Go to https://x.com/<your-handle>/lists, then paste this script again.');
return;
}
const users = CONFIG.usernames.map(u => u.replace(/^@/, '').trim()).filter(Boolean);
if (users.length === 0) {
log.warning('CONFIG.usernames is empty. Add at least one handle to CONFIG.usernames.');
return;
}
log.info(`Target list: "${CONFIG.listName}"`);
log.info(`Users to add: ${users.length}`);
if (CONFIG.dryRun) {
log.warning('DRY RUN is ON. No one will be added. Set CONFIG.dryRun = false to execute.');
users.forEach(u => log.step(`Would add @${u}`));
console.log('\n✅ Dry run complete. by nichxbt');
return { dryRun: true, wouldAdd: users };
}
log.info('To stop early: window.stopAddToList()');
console.log('');
// 1) Open the target list
const listLink = findListLinkByName(CONFIG.listName);
if (!listLink) {
log.error(`Could not find a list named "${CONFIG.listName}" on this page.`);
log.info('Confirm the name matches exactly and you are on x.com/<you>/lists.');
return;
}
log.step(`Opening list "${CONFIG.listName}"`);
listLink.click();
await sleep(CONFIG.stepDelay);
// 2) Open Edit -> Manage members -> Add members
const editBtn = await waitFor(SELECTORS.editButton, 5000) || findButtonByText(['edit list']);
if (editBtn) { editBtn.click(); await sleep(CONFIG.stepDelay); }
const manageBtn = await waitFor(SELECTORS.manageMembers, 4000) || findButtonByText(['manage members']);
if (manageBtn) { manageBtn.click(); await sleep(CONFIG.stepDelay); }
const addTab = await waitFor(SELECTORS.addMembersTab, 4000) || findButtonByText(['add members', 'suggested']);
if (addTab) { addTab.click(); await sleep(CONFIG.stepDelay); }
const searchInput = await waitFor(SELECTORS.searchInput, 5000);
if (!searchInput) {
log.error('Could not open the "Add members" search box.');
log.info('Open the list, tap Edit List then Manage members, then rerun this script.');
return;
}
// 3) Add each user, one at a time, rate-limited
for (const username of users) {
if (stopped) break;
try {
searchInput.focus();
setReactInput(searchInput, '');
await sleep(300);
setReactInput(searchInput, username);
await sleep(CONFIG.searchWait);
const cells = document.querySelectorAll(SELECTORS.userCell);
let matchedCell = null;
for (const cell of cells) {
if (handleFromCell(cell).toLowerCase() === username.toLowerCase()) { matchedCell = cell; break; }
}
// Fall back to a looser text match if the handle link was not resolvable
if (!matchedCell) {
for (const cell of cells) {
if ((cell.textContent || '').toLowerCase().includes('@' + username.toLowerCase())) { matchedCell = cell; break; }
}
}
if (!matchedCell) {
stats.notFound++;
log.warning(`@${username} not found in search results`);
continue;
}
// Already a member? X labels the row control "Remove" when present.
const control = matchedCell.querySelector(SELECTORS.addButton);
const controlText = control ? (control.textContent || '').trim().toLowerCase() : '';
if (controlText.includes('remove')) {
stats.alreadyIn++;
log.info(`@${username} is already in the list, skipping`);
continue;
}
if (control) control.click();
else matchedCell.click();
stats.added++;
log.success(`Added #${stats.added}: @${username}`);
await sleep(randomDelay());
} catch (e) {
stats.errors++;
log.error(`Error adding @${username}: ${e.message}`);
}
}
// 4) Save / close the manage-members dialog if a save control is present
const saveBtn = findButtonByText(['done', 'save']);
if (saveBtn) { try { saveBtn.click(); } catch (e) {} }
// ============================================
// 📊 SUMMARY
// ============================================
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ 📊 ADD TO LIST - COMPLETE ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log(` ✅ Added: ${stats.added}`);
console.log(` ⏭️ Already in: ${stats.alreadyIn}`);
console.log(` 🔍 Not found: ${stats.notFound}`);
console.log(` ❌ Errors: ${stats.errors}`);
console.log('✅ Script completed! by nichxbt');
return stats;
})();
});
register("audience-overlap", function(){
(async function audienceOverlap() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// PRIMARY MODE: paste two arrays of handles here (with or without the @).
// Example: ['alice', 'bob', '@carol']
listA: [],
listB: [],
// Friendly labels for the two sets (used in the report and filename).
labelA: 'A',
labelB: 'B',
// OPTIONAL MODE: if the arrays above are empty, read handles from these
// localStorage keys instead. Each key must hold a JSON array of handles
// or an array of objects with a `username`/`handle`/`screen_name` field.
storageKeyA: '',
storageKeyB: '',
// How many handle examples to print per section.
printLimit: 20,
// Auto-download a JSON report when finished.
exportResults: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🛠️ HELPERS
// ============================================
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const download = (data, filename) => {
try {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
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);
log.success(`Report downloaded: ${filename}`);
} catch (e) {
log.error(`Could not download report: ${e.message}`);
}
};
// Turn any handle-ish value into a clean lowercase key. Accepts strings
// ("@alice", "/alice", "https://x.com/alice") and objects from scrapers.
const normalizeHandle = (raw) => {
let value = raw;
if (value && typeof value === 'object') {
value = value.username || value.handle || value.screen_name || value.screenName || '';
}
if (typeof value !== 'string') return null;
let handle = value.trim();
if (!handle) return null;
const urlMatch = handle.match(/(?:x\.com|twitter\.com)\/([A-Za-z0-9_]+)/i);
if (urlMatch) handle = urlMatch[1];
handle = handle.replace(/^\//, '').replace(/^@/, '').split(/[/?#]/)[0].trim();
if (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) return null;
return handle.toLowerCase();
};
// Build a Map of normalizedKey -> original display handle, dropping dupes.
const toHandleMap = (list) => {
const map = new Map();
(Array.isArray(list) ? list : []).forEach((raw) => {
const key = normalizeHandle(raw);
if (!key || map.has(key)) return;
const display = (raw && typeof raw === 'object')
? (raw.username || raw.handle || raw.screen_name || raw.screenName || key)
: key;
map.set(key, String(display).replace(/^@/, ''));
});
return map;
};
const readStorageList = (key) => {
if (!key) return null;
try {
const parsed = JSON.parse(localStorage.getItem(key) || 'null');
if (Array.isArray(parsed)) return parsed;
if (parsed && Array.isArray(parsed.followers)) return parsed.followers;
if (parsed && Array.isArray(parsed.handles)) return parsed.handles;
return null;
} catch (e) {
log.warning(`Could not read localStorage key "${key}": ${e.message}`);
return null;
}
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔀 AUDIENCE OVERLAP - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Resolve the two source lists in priority order:
// 1) CONFIG arrays 2) localStorage keys 3) interactive prompt.
let rawA = Array.isArray(CONFIG.listA) && CONFIG.listA.length ? CONFIG.listA : null;
let rawB = Array.isArray(CONFIG.listB) && CONFIG.listB.length ? CONFIG.listB : null;
if (!rawA && CONFIG.storageKeyA) rawA = readStorageList(CONFIG.storageKeyA);
if (!rawB && CONFIG.storageKeyB) rawB = readStorageList(CONFIG.storageKeyB);
if (!rawA || !rawB) {
log.warning('No lists in CONFIG and no usable localStorage keys. Falling back to a prompt.');
if (!rawA) {
const inputA = window.prompt(`Paste comma-separated handles for set "${CONFIG.labelA}":`, '');
rawA = inputA ? inputA.split(/[\s,]+/) : [];
}
if (!rawB) {
const inputB = window.prompt(`Paste comma-separated handles for set "${CONFIG.labelB}":`, '');
rawB = inputB ? inputB.split(/[\s,]+/) : [];
}
}
const mapA = toHandleMap(rawA);
const mapB = toHandleMap(rawB);
if (mapA.size === 0 || mapB.size === 0) {
log.error('Both sets need at least one valid handle.');
log.info(`Parsed: "${CONFIG.labelA}" = ${mapA.size} handles, "${CONFIG.labelB}" = ${mapB.size} handles.`);
log.info('Set CONFIG.listA and CONFIG.listB to arrays of handles and run again.');
return;
}
const keysA = new Set(mapA.keys());
const keysB = new Set(mapB.keys());
const shared = [...keysA].filter((k) => keysB.has(k));
const onlyA = [...keysA].filter((k) => !keysB.has(k));
const onlyB = [...keysB].filter((k) => !keysA.has(k));
const union = new Set([...keysA, ...keysB]);
const jaccardPct = Math.round((shared.length / union.size) * 1000) / 10;
const sharedOfAPct = Math.round((shared.length / keysA.size) * 1000) / 10;
const sharedOfBPct = Math.round((shared.length / keysB.size) * 1000) / 10;
const displayA = (k) => '@' + (mapA.get(k) || k);
const displayB = (k) => '@' + (mapB.get(k) || k);
// ============================================
// 📊 SUMMARY
// ============================================
console.log('');
console.log('━'.repeat(60));
console.log('🔀 AUDIENCE OVERLAP');
console.log('━'.repeat(60));
console.log(` Set "${CONFIG.labelA}": ${keysA.size} unique handles`);
console.log(` Set "${CONFIG.labelB}": ${keysB.size} unique handles`);
console.log('');
console.log(` Shared: ${shared.length}`);
console.log(` Only in ${CONFIG.labelA}: ${onlyA.length}`);
console.log(` Only in ${CONFIG.labelB}: ${onlyB.length}`);
console.log('');
console.log(` Overlap (Jaccard): ${jaccardPct}%`);
console.log(` Shared as % of "${CONFIG.labelA}": ${sharedOfAPct}%`);
console.log(` Shared as % of "${CONFIG.labelB}": ${sharedOfBPct}%`);
const preview = (keys, disp) => keys.slice(0, CONFIG.printLimit).map(disp).join(', ') + (keys.length > CONFIG.printLimit ? ` ... (+${keys.length - CONFIG.printLimit} more)` : '');
if (shared.length > 0) {
console.log('');
console.log(`👥 SHARED (${shared.length}):`);
console.log(' ' + preview(shared, displayA));
}
console.log('');
console.log(`🅰️ ONLY IN "${CONFIG.labelA}" (${onlyA.length}):`);
console.log(' ' + (onlyA.length ? preview(onlyA, displayA) : '(none)'));
console.log('');
console.log(`🅱️ ONLY IN "${CONFIG.labelB}" (${onlyB.length}):`);
console.log(' ' + (onlyB.length ? preview(onlyB, displayB) : '(none)'));
console.log('');
if (jaccardPct > 50) log.info('HIGH overlap. These audiences are largely the same crowd.');
else if (jaccardPct > 20) log.info('MODERATE overlap. Cross-promotion would reach some new people.');
else log.info('LOW overlap. Big opportunity for cross-pollination.');
const report = {
labelA: CONFIG.labelA,
labelB: CONFIG.labelB,
analyzedAt: new Date().toISOString(),
counts: {
setA: keysA.size,
setB: keysB.size,
shared: shared.length,
onlyA: onlyA.length,
onlyB: onlyB.length,
union: union.size
},
overlap: { jaccardPct, sharedOfAPct, sharedOfBPct },
sharedHandles: shared.map(displayA).map((h) => h.slice(1)),
uniqueToA: onlyA.map(displayA).map((h) => h.slice(1)),
uniqueToB: onlyB.map(displayB).map((h) => h.slice(1))
};
if (CONFIG.exportResults) {
console.log('');
const safe = (s) => String(s).replace(/[^A-Za-z0-9_-]/g, '');
download(report, `xactions-overlap-${safe(CONFIG.labelA)}-vs-${safe(CONFIG.labelB)}.json`);
}
window.xactionsOverlap = report;
console.log('');
log.info('Full report object: window.xactionsOverlap');
log.success('Done.');
return report;
})();
});
register("audit-followers", function(){
var CONFIG = {
// Scroll settings
scrollDelay: 1500,
maxScrolls: 100,
maxRetries: 5,
// Analysis thresholds
thresholds: {
// Ratio categories
influencer: 10, // Followers 10x+ following = influencer
balanced: 0.5, // Ratio between 0.5 and 2 = balanced
aggressive: 0.1, // Ratio below 0.1 = aggressive follower
// Following counts
massFollower: 2000, // Following 2000+
selectiveFollower: 100 // Following less than 100
}
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function auditFollowers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🔍 XActions — Audit Followers ║
║ Comprehensive follower quality analysis ║
╚══════════════════════════════════════════════════════════════╝
`);
if (!window.location.pathname.includes('/followers')) {
console.error('❌ Please navigate to your FOLLOWERS page first!');
console.log('👉 Go to: https://x.com/YOUR_USERNAME/followers');
return;
}
const username = window.location.pathname.split('/')[1];
const $userCell = '[data-testid="UserCell"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '');
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
console.log(`🔍 Auditing followers for @${username}\n`);
console.log('⏳ This may take a while for large accounts...\n');
const followers = [];
const scanned = new Set();
let retries = 0;
let scrollCount = 0;
while (scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = scanned.size;
document.querySelectorAll($userCell).forEach(cell => {
const link = cell.querySelector('a[href^="/"]');
const user = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!user || scanned.has(user)) return;
scanned.add(user);
// Get display name
const nameEl = cell.querySelector('[dir="ltr"] span');
const displayName = nameEl?.textContent || user;
// Get bio
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const bio = bioEl?.textContent || '';
// Parse stats
const text = cell.textContent;
const followingMatch = text.match(/([\d,.]+[KMB]?)\s*Following/i);
const followersMatch = text.match(/([\d,.]+[KMB]?)\s*Follower/i);
const following = followingMatch ? parseCount(followingMatch[1]) : 0;
const followerCount = followersMatch ? parseCount(followersMatch[1]) : 0;
// Check for verified (testid is locale-independent; aria-label kept as fallback)
const verified = cell.querySelector('[data-testid="icon-verified"], svg[aria-label*="Verified"]') !== null;
// Check for default avatar
const defaultAvatar = cell.querySelector('img[src*="default_profile"]') !== null;
followers.push({
username: user,
displayName,
bio,
following,
followers: followerCount,
ratio: followerCount > 0 ? following / followerCount : Infinity,
verified,
defaultAvatar,
hasBio: bio.length > 0
});
});
if (scanned.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scanned.size} followers...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
if (followers.length === 0) {
console.error('❌ No followers found!');
return;
}
console.log(`\n✅ Audit complete! Analyzed ${followers.length} followers\n`);
// === ANALYSIS ===
// Categorize by type
const categories = {
influencers: followers.filter(f => f.followers > 0 && f.followers / (f.following || 1) >= CONFIG.thresholds.influencer),
balanced: followers.filter(f => {
const ratio = (f.followers || 1) / (f.following || 1);
return ratio >= CONFIG.thresholds.balanced && ratio < CONFIG.thresholds.influencer;
}),
aggressiveFollowers: followers.filter(f => f.following > CONFIG.thresholds.massFollower),
verified: followers.filter(f => f.verified),
defaultAvatars: followers.filter(f => f.defaultAvatar),
noBio: followers.filter(f => !f.hasBio),
zeroFollowers: followers.filter(f => f.followers === 0),
highQuality: followers.filter(f => f.followers >= 1000 && f.hasBio && !f.defaultAvatar)
};
// Calculate averages
const avgFollowing = followers.reduce((sum, f) => sum + f.following, 0) / followers.length;
const avgFollowers = followers.reduce((sum, f) => sum + f.followers, 0) / followers.length;
const totalReach = followers.reduce((sum, f) => sum + f.followers, 0);
const formatNum = (n) => {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return Math.round(n).toString();
};
// Output
console.log('═'.repeat(60));
console.log(`📊 FOLLOWER AUDIT REPORT: @${username}`);
console.log('═'.repeat(60));
console.log('\n📈 OVERVIEW:');
console.log('─'.repeat(50));
console.log(` Total followers analyzed: ${followers.length.toLocaleString()}`);
console.log(` Total audience reach: ${formatNum(totalReach)}`);
console.log(` Average following: ${formatNum(avgFollowing)}`);
console.log(` Average followers: ${formatNum(avgFollowers)}`);
console.log('\n👥 FOLLOWER COMPOSITION:');
console.log('─'.repeat(50));
console.log(` ⭐ Verified accounts: ${categories.verified.length} (${(categories.verified.length/followers.length*100).toFixed(1)}%)`);
console.log(` 🌟 Influencers (10:1+): ${categories.influencers.length} (${(categories.influencers.length/followers.length*100).toFixed(1)}%)`);
console.log(` 💎 High-quality: ${categories.highQuality.length} (${(categories.highQuality.length/followers.length*100).toFixed(1)}%)`);
console.log(` ⚖️ Balanced accounts: ${categories.balanced.length} (${(categories.balanced.length/followers.length*100).toFixed(1)}%)`);
console.log(` 🔄 Aggressive followers: ${categories.aggressiveFollowers.length} (${(categories.aggressiveFollowers.length/followers.length*100).toFixed(1)}%)`);
console.log('\n⚠️ RED FLAGS:');
console.log('─'.repeat(50));
console.log(` 🖼️ Default avatars: ${categories.defaultAvatars.length} (${(categories.defaultAvatars.length/followers.length*100).toFixed(1)}%)`);
console.log(` 📝 No bio: ${categories.noBio.length} (${(categories.noBio.length/followers.length*100).toFixed(1)}%)`);
console.log(` 👻 Zero followers: ${categories.zeroFollowers.length} (${(categories.zeroFollowers.length/followers.length*100).toFixed(1)}%)`);
// Quality Score
const qualityScore = Math.min(100, Math.round(
(categories.highQuality.length * 3 +
categories.influencers.length * 2 +
categories.verified.length * 5 +
categories.balanced.length * 1) /
(followers.length * 3) * 100
));
console.log('\n🏆 QUALITY SCORE:');
console.log('─'.repeat(50));
const scoreBar = '█'.repeat(Math.round(qualityScore / 5)) + '░'.repeat(20 - Math.round(qualityScore / 5));
console.log(` [${scoreBar}] ${qualityScore}/100`);
if (qualityScore >= 80) console.log(' 🏆 EXCELLENT - Premium quality audience!');
else if (qualityScore >= 60) console.log(' 👍 GOOD - Solid audience quality');
else if (qualityScore >= 40) console.log(' ⚠️ MODERATE - Room for improvement');
else console.log(' 🚨 POOR - Consider audience cleanup');
// Top followers by reach
if (categories.influencers.length > 0) {
console.log('\n' + '═'.repeat(60));
console.log('🌟 YOUR TOP INFLUENTIAL FOLLOWERS');
console.log('═'.repeat(60));
const topInfluencers = categories.influencers
.sort((a, b) => b.followers - a.followers)
.slice(0, 15);
topInfluencers.forEach((f, i) => {
const badge = f.verified ? ' ✓' : '';
console.log(`\n${i + 1}. ${f.displayName}${badge} (@${f.username})`);
console.log(` ${formatNum(f.followers)} followers | ${formatNum(f.following)} following`);
if (f.bio) console.log(` "${f.bio.slice(0, 60)}${f.bio.length > 60 ? '...' : ''}"`);
});
}
// Verified followers
if (categories.verified.length > 0) {
console.log('\n' + '═'.repeat(60));
console.log('⭐ VERIFIED FOLLOWERS');
console.log('═'.repeat(60));
categories.verified.slice(0, 20).forEach((f, i) => {
console.log(` ${i + 1}. @${f.username} — ${formatNum(f.followers)} followers`);
});
}
// Bio keyword analysis
console.log('\n' + '═'.repeat(60));
console.log('🏷️ AUDIENCE INTERESTS (Bio Keywords)');
console.log('═'.repeat(60));
const keywords = {};
const interestCategories = {
tech: ['developer', 'engineer', 'programmer', 'tech', 'software', 'coding', 'ai', 'data'],
crypto: ['crypto', 'bitcoin', 'btc', 'eth', 'nft', 'web3', 'defi', 'blockchain'],
business: ['founder', 'ceo', 'entrepreneur', 'startup', 'investor', 'business'],
marketing: ['marketing', 'growth', 'social media', 'brand', 'digital'],
creative: ['creator', 'artist', 'designer', 'writer', 'photographer', 'content'],
finance: ['finance', 'trading', 'forex', 'investment', 'stocks']
};
followers.forEach(f => {
const bioLower = f.bio.toLowerCase();
Object.entries(interestCategories).forEach(([category, words]) => {
if (words.some(w => bioLower.includes(w))) {
keywords[category] = (keywords[category] || 0) + 1;
}
});
});
const sortedInterests = Object.entries(keywords)
.sort((a, b) => b[1] - a[1]);
if (sortedInterests.length > 0) {
const maxCount = sortedInterests[0][1];
sortedInterests.forEach(([category, count]) => {
const bar = '█'.repeat(Math.round(count / maxCount * 20));
const pct = (count / followers.length * 100).toFixed(1);
console.log(` ${category.padEnd(12)} ${bar.padEnd(20)} ${count} (${pct}%)`);
});
} else {
console.log(' Not enough bio data to analyze interests');
}
// Save audit results
const storageKey = `xactions_audit_${username}`;
const data = {
username,
timestamp: new Date().toISOString(),
totalFollowers: followers.length,
qualityScore,
totalReach,
composition: {
verified: categories.verified.length,
influencers: categories.influencers.length,
highQuality: categories.highQuality.length,
defaultAvatars: categories.defaultAvatars.length,
noBio: categories.noBio.length,
zeroFollowers: categories.zeroFollowers.length
},
interests: Object.fromEntries(sortedInterests),
topInfluencers: categories.influencers
.sort((a, b) => b.followers - a.followers)
.slice(0, 50)
.map(f => ({ username: f.username, followers: f.followers, verified: f.verified }))
};
localStorage.setItem(storageKey, JSON.stringify(data));
// Historical comparison
console.log('\n' + '═'.repeat(60));
console.log('💾 AUDIT SAVED');
console.log('═'.repeat(60));
console.log(`\n Run this audit periodically to track audience quality.`);
console.log(` Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('\n' + '═'.repeat(60));
console.log('💡 RECOMMENDATIONS');
console.log('═'.repeat(60));
if (categories.defaultAvatars.length / followers.length > 0.2) {
console.log('\n⚠️ High number of default avatars - consider removing bots');
}
if (categories.zeroFollowers.length / followers.length > 0.1) {
console.log('\n⚠️ Many followers have 0 followers - possible fake accounts');
}
if (categories.influencers.length > 0) {
console.log(`\n✅ You have ${categories.influencers.length} influencer followers - engage with them!`);
}
if (categories.verified.length > 0) {
console.log(`\n✅ You have ${categories.verified.length} verified followers - great credibility!`);
}
console.log('\n' + '═'.repeat(60) + '\n');
})();
});
register("auto-commenter", function(){
var CONFIG = {
// ---- COMMENTS TO POST ----
// The script will randomly pick from these
// 💡 Add variety to avoid looking like a bot!
comments: [
'🔥',
'Great point!',
'This is so true 👏',
'Interesting perspective!',
'Thanks for sharing this 🙏',
'💯',
'Well said!',
'Couldn\'t agree more',
'👀 interesting',
'This is gold ✨'
],
// ---- LIMITS ----
// Maximum comments per session
maxComments: 5,
// Only comment on tweets posted within this window
maxPostAgeMinutes: 60,
// Minimum post age (to avoid commenting too fast)
minPostAgeSeconds: 30,
// ---- BEHAVIOR ----
// Only comment on original tweets (skip replies)
onlyOriginalTweets: true,
// Only comment on tweets with media (images/videos)
onlyWithMedia: false,
// ---- TIMING ----
// Delay between comments (milliseconds)
minDelay: 30000, // 30 seconds minimum!
maxDelay: 60000, // 60 seconds
// Scroll delay
scrollDelay: 2000
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function autoCommenter() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
const randomComment = () => CONFIG.comments[Math.floor(Math.random() * CONFIG.comments.length)];
// DOM Selectors
const $tweet = 'article[data-testid="tweet"]';
const $replyButton = '[data-testid="reply"]';
const $tweetTextarea = '[data-testid="tweetTextarea_0"]';
const $tweetButton = '[data-testid="tweetButton"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 💬 AUTO COMMENTER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Get target username from URL
const pathMatch = window.location.pathname.match(/^\/([^\/]+)/);
const targetUser = pathMatch ? pathMatch[1] : null;
if (!targetUser || ['home', 'explore', 'search', 'notifications', 'messages', 'i'].includes(targetUser)) {
console.error('❌ ERROR: Must be on a user\'s profile page!');
console.log('📍 Go to: https://x.com/TARGET_USERNAME');
return;
}
console.log(`👤 Target: @${targetUser}`);
console.log(`💬 Max comments: ${CONFIG.maxComments}`);
console.log(`⏱️ Post age window: ${CONFIG.minPostAgeSeconds}s - ${CONFIG.maxPostAgeMinutes}min`);
console.log('');
const commentedTweets = new Set();
// Load previously commented from storage
const STORAGE_KEY = `xactions_commented_${targetUser}`;
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
JSON.parse(saved).forEach(id => commentedTweets.add(id));
console.log(`📚 Loaded ${commentedTweets.size} previously commented tweets`);
}
} catch (e) {}
let totalCommented = 0;
/**
* Get tweet ID
*/
function getTweetId(tweetEl) {
// The timestamp's enclosing anchor is the tweet's own permalink; the first
// /status/ link in the article can belong to a quoted tweet
const timeEl = tweetEl.querySelector('time');
const link = (timeEl && timeEl.closest('a[href*="/status/"]')) ||
tweetEl.querySelector('a[href*="/status/"]');
if (link) {
const match = link.href.match(/\/status\/(\d+)/);
return match ? match[1] : null;
}
return null;
}
/**
* Check tweet age
*/
function getTweetAge(tweetEl) {
const timeEl = tweetEl.querySelector('time');
if (!timeEl) return null;
const datetime = timeEl.getAttribute('datetime');
if (!datetime) return null;
return (Date.now() - new Date(datetime).getTime()) / 1000; // seconds
}
/**
* Check if tweet matches criteria
*/
function matchesCriteria(tweetEl) {
const age = getTweetAge(tweetEl);
if (!age) return false;
// Check age window
if (age < CONFIG.minPostAgeSeconds) return false;
if (age > CONFIG.maxPostAgeMinutes * 60) return false;
// Check if original tweet (structural marker first; English text as fallback)
if (CONFIG.onlyOriginalTweets) {
const isReply = tweetEl.querySelector('[data-testid="in-reply-to"]') !== null ||
Array.from(tweetEl.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
if (isReply) return false;
}
// Check for media
if (CONFIG.onlyWithMedia) {
const hasMedia = tweetEl.querySelector('[data-testid="tweetPhoto"]') ||
tweetEl.querySelector('[data-testid="videoPlayer"], [data-testid="videoComponent"]');
if (!hasMedia) return false;
}
return true;
}
console.log('🚀 Looking for tweets to comment on...');
console.log('');
// Scroll through profile
let scrolls = 0;
const maxScrolls = 20;
while (totalCommented < CONFIG.maxComments && scrolls < maxScrolls) {
const tweets = document.querySelectorAll($tweet);
for (const tweet of tweets) {
if (totalCommented >= CONFIG.maxComments) break;
const tweetId = getTweetId(tweet);
if (!tweetId || commentedTweets.has(tweetId)) continue;
if (!matchesCriteria(tweet)) continue;
// Find reply button
const replyBtn = tweet.querySelector($replyButton);
if (!replyBtn) continue;
try {
console.log(`💬 Commenting on tweet ${tweetId}...`);
// Click reply
replyBtn.click();
await sleep(1000);
// Find textarea
const textarea = document.querySelector($tweetTextarea);
if (!textarea) {
console.warn('⚠️ Could not find reply textarea');
continue;
}
// Type comment
const comment = randomComment();
textarea.focus();
document.execCommand('insertText', false, comment);
await sleep(500);
// Click tweet button
const tweetBtn = document.querySelector($tweetButton);
if (tweetBtn && !tweetBtn.disabled) {
tweetBtn.click();
commentedTweets.add(tweetId);
totalCommented++;
console.log(`✅ Posted: "${comment}"`);
// Save to storage
localStorage.setItem(STORAGE_KEY, JSON.stringify([...commentedTweets]));
// Wait before next comment
const delay = randomDelay();
console.log(`⏳ Waiting ${Math.round(delay/1000)}s before next comment...`);
await sleep(delay);
} else {
console.warn('⚠️ Tweet button not available');
// Close dialog
const closeBtn = document.querySelector('[data-testid="app-bar-close"]');
if (closeBtn) closeBtn.click();
}
} catch (e) {
console.warn('⚠️ Error:', e.message);
// Try to close any open dialogs
const closeBtn = document.querySelector('[data-testid="app-bar-close"]');
if (closeBtn) closeBtn.click();
}
}
if (totalCommented >= CONFIG.maxComments) break;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ AUTO COMMENTER COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`💬 Total comments posted: ${totalCommented}`);
console.log('');
return { commented: totalCommented };
})();
});
register("auto-liker", function(){
var CONFIG = {
// ---- TARGETING ----
// Like ALL tweets (ignores keywords filter)
// 💡 Set to false to only like tweets matching keywords
likeAll: false,
// Only like tweets containing these words (case-insensitive)
// 💡 Leave empty [] and set likeAll: true to like everything
keywords: ['web3', 'crypto', 'AI', 'startup'],
// Only like tweets from these specific users (leave empty for any user)
// 💡 Example: ['elonmusk', 'naval']
fromUsers: [],
// ---- LIMITS ----
// Maximum tweets to like
maxLikes: 20,
// Maximum scroll depth
maxScrolls: 50,
// ---- BEHAVIOR ----
// Also retweet liked posts
alsoRetweet: false,
// Skip replies (only like original tweets)
skipReplies: true,
// Skip promoted/ad tweets
skipAds: true,
// ---- TIMING ----
// Random delay between likes (milliseconds)
minDelay: 2000,
maxDelay: 5000,
// Scroll delay
scrollDelay: 2000
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function autoLiker() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
// DOM Selectors
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
const $likeButton = '[data-testid="like"]';
const $unlikeButton = '[data-testid="unlike"]';
const $retweetButton = '[data-testid="retweet"]';
const $confirmRetweet = '[data-testid="retweetConfirm"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ❤️ AUTO LIKER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
console.log(`🎯 Mode: ${CONFIG.likeAll ? 'Like ALL tweets' : 'Keyword filter'}`);
if (!CONFIG.likeAll && CONFIG.keywords.length > 0) {
console.log(`🔍 Keywords: ${CONFIG.keywords.join(', ')}`);
}
if (CONFIG.fromUsers.length > 0) {
console.log(`👤 From users: ${CONFIG.fromUsers.join(', ')}`);
}
console.log(`📊 Max likes: ${CONFIG.maxLikes}`);
console.log('');
const likedTweets = new Set();
let totalLiked = 0;
let totalRetweeted = 0;
let scrolls = 0;
/**
* Check if tweet matches filters
*/
function matchesFilters(tweetEl) {
const textEl = tweetEl.querySelector($tweetText);
const text = textEl ? textEl.innerText.toLowerCase() : '';
// Check if from specific users (User-Name block is the author; the first
// profile link in the article can be a reposter's socialContext link)
if (CONFIG.fromUsers.length > 0) {
const userLink = tweetEl.querySelector('[data-testid="User-Name"] a[href^="/"]') ||
tweetEl.querySelector('a[href^="/"]');
const username = userLink ? userLink.getAttribute('href').replace('/', '').toLowerCase() : '';
if (!CONFIG.fromUsers.some(u => u.toLowerCase() === username)) {
return false;
}
}
// Check keywords
if (!CONFIG.likeAll && CONFIG.keywords.length > 0) {
if (!CONFIG.keywords.some(k => text.includes(k.toLowerCase()))) {
return false;
}
}
// Skip replies (structural marker first; English text as fallback)
if (CONFIG.skipReplies) {
const isReply = tweetEl.querySelector('[data-testid="in-reply-to"]') !== null ||
Array.from(tweetEl.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
if (isReply) return false;
}
// Skip ads: placementTracking wraps promoted tweets; the label check needs
// an exact span match ("Ad" as a substring hits words like "Advice")
if (CONFIG.skipAds) {
const isAd = tweetEl.querySelector('[data-testid="placementTracking"]') !== null ||
Array.from(tweetEl.querySelectorAll('span')).some(s => {
const t = s.innerText.trim();
return t === 'Ad' || t === 'Promoted';
});
if (isAd) return false;
}
return true;
}
/**
* Get tweet ID
*/
function getTweetId(tweetEl) {
// The timestamp's enclosing anchor is the tweet's own permalink; the first
// /status/ link in the article can belong to a quoted tweet
const timeEl = tweetEl.querySelector('time');
const link = (timeEl && timeEl.closest('a[href*="/status/"]')) ||
tweetEl.querySelector('a[href*="/status/"]');
if (link) {
const match = link.href.match(/\/status\/(\d+)/);
return match ? match[1] : null;
}
return null;
}
console.log('🚀 Starting auto-liker...');
console.log('');
while (totalLiked < CONFIG.maxLikes && scrolls < CONFIG.maxScrolls) {
const tweets = document.querySelectorAll($tweet);
for (const tweet of tweets) {
if (totalLiked >= CONFIG.maxLikes) break;
const tweetId = getTweetId(tweet);
if (!tweetId || likedTweets.has(tweetId)) continue;
// Check if already liked
const unlikeBtn = tweet.querySelector($unlikeButton);
if (unlikeBtn) {
likedTweets.add(tweetId);
continue;
}
// Check filters
if (!matchesFilters(tweet)) continue;
// Find like button
const likeBtn = tweet.querySelector($likeButton);
if (!likeBtn) continue;
try {
// Like the tweet
likeBtn.click();
likedTweets.add(tweetId);
totalLiked++;
const textEl = tweet.querySelector($tweetText);
const preview = textEl ? textEl.innerText.substring(0, 40) + '...' : '[No text]';
console.log(`❤️ Liked #${totalLiked}: "${preview}"`);
// Also retweet if enabled
if (CONFIG.alsoRetweet) {
await sleep(500);
const rtBtn = tweet.querySelector($retweetButton);
if (rtBtn) {
rtBtn.click();
await sleep(300);
const confirmBtn = document.querySelector($confirmRetweet);
if (confirmBtn) {
confirmBtn.click();
totalRetweeted++;
console.log(` 🔄 Retweeted`);
}
}
}
await sleep(randomDelay());
} catch (e) {
console.warn('⚠️ Error:', e.message);
}
}
// Scroll for more
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
if (scrolls % 10 === 0) {
console.log(`📜 Scrolled ${scrolls} times, liked ${totalLiked}...`);
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ AUTO LIKER COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`❤️ Total liked: ${totalLiked}`);
if (CONFIG.alsoRetweet) {
console.log(`🔄 Total retweeted: ${totalRetweeted}`);
}
console.log('');
return { liked: totalLiked, retweeted: totalRetweeted };
})();
});
register("auto-reply-dms", function(){
(async function autoReplyDMs() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// The reply to send. {name} is filled from the conversation title.
message: `Thanks for reaching out! I'll get back to you soon. 🙏`,
// Safety limits
maxReplies: 15, // hard cap on replies sent this run
onlyUnread: true, // only reply to conversations marked unread
// Timing (randomized between min and max, per reply)
minDelay: 4000,
maxDelay: 9000,
// Scrolling the conversation list to load more threads
maxScrollRounds: 12,
scrollDelay: 1500
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
conversation: '[data-testid="conversation"]',
unreadBadge: '[data-testid="unread"]',
messageInput: '[data-testid="dmComposerTextInput"]',
sendButton: '[data-testid="dmComposerSendButton"]',
backButton: '[data-testid="app-bar-back"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const fillTemplate = (tpl, name) => tpl.split('{name}').join(name || 'there');
// A stable-ish id for a conversation row: its data-testid href or its text.
const convoKey = (conv) => {
const link = conv.querySelector('a[href*="/messages/"]');
if (link) return link.getAttribute('href');
return (conv.textContent || '').slice(0, 80);
};
const isUnread = (conv) => {
if (conv.querySelector(SELECTORS.unreadBadge)) return true;
// Fallback: X flags unread rows on the aria attributes of the row.
return conv.getAttribute('aria-live') === 'polite' && /unread/i.test(conv.getAttribute('aria-label') || '');
};
const convoName = (conv) => {
const span = conv.querySelector('[dir="ltr"] span, span');
return span ? span.textContent.trim() : '';
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} replies sent`)
};
// ============================================
// 🎯 STATE
// ============================================
const stats = { replied: 0, skippedRead: 0, noInput: 0, errors: 0 };
const handled = new Set();
// Stop switch: run window.stopAutoReplyDMs() to abort after the current thread.
let stopped = false;
window.stopAutoReplyDMs = () => {
stopped = true;
log.warning('Stop requested. Finishing the current reply, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🤖 AUTO REPLY DMs - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
log.info('To stop early: window.stopAutoReplyDMs()');
// Page guard
if (!/\/messages/.test(window.location.pathname)) {
log.error('Not on the Messages page. Go to https://x.com/messages and re-run.');
return stats;
}
log.info(`Only unread: ${CONFIG.onlyUnread}`);
log.info(`Max replies this run: ${CONFIG.maxReplies}`);
// ============================================
// 📬 REPLY IN THE OPEN THREAD
// ============================================
const replyInOpenThread = async (name) => {
// Poll briefly for the composer to render after opening a thread.
let input = document.querySelector(SELECTORS.messageInput);
let waited = 0;
while (!input && waited < 4000) {
await sleep(400);
waited += 400;
input = document.querySelector(SELECTORS.messageInput);
}
if (!input) {
stats.noInput++;
log.warning(`No composer for "${name}" (they may not allow replies).`);
return false;
}
const message = fillTemplate(CONFIG.message, name);
input.focus();
document.execCommand('insertText', false, message);
await sleep(900);
const sendBtn = document.querySelector(SELECTORS.sendButton);
if (!sendBtn || sendBtn.getAttribute('aria-disabled') === 'true' || sendBtn.disabled) {
stats.noInput++;
log.warning(`Send button unavailable for "${name}".`);
return false;
}
sendBtn.click();
await sleep(1200);
return true;
};
// ============================================
// 🔁 MAIN LOOP
// ============================================
let scrollRounds = 0;
let emptyRounds = 0;
while (!stopped && stats.replied < CONFIG.maxReplies && scrollRounds < CONFIG.maxScrollRounds) {
const convos = Array.from(document.querySelectorAll(SELECTORS.conversation));
if (convos.length === 0) {
log.warning('No conversations visible. Is the Messages inbox open?');
break;
}
let foundNew = false;
for (const conv of convos) {
if (stopped || stats.replied >= CONFIG.maxReplies) break;
const key = convoKey(conv);
if (handled.has(key)) continue;
handled.add(key);
foundNew = true;
const name = convoName(conv);
try {
if (CONFIG.onlyUnread && !isUnread(conv)) {
stats.skippedRead++;
continue;
}
conv.click();
await sleep(1600);
const ok = await replyInOpenThread(name);
if (ok) {
stats.replied++;
log.success(`Replied to "${name || 'conversation'}"`);
log.progress(stats.replied, CONFIG.maxReplies);
await randomDelay();
}
// Return to the inbox list so the next row is clickable on narrow layouts.
const back = document.querySelector(SELECTORS.backButton);
if (back) {
back.click();
await sleep(1000);
}
} catch (error) {
log.error(`Error on "${name}": ${error.message}`);
stats.errors++;
const back = document.querySelector(SELECTORS.backButton);
if (back) { back.click(); await sleep(800); }
}
}
if (!foundNew) {
emptyRounds++;
if (emptyRounds >= 3) {
log.info('No new conversations after several scrolls. Stopping.');
break;
}
} else {
emptyRounds = 0;
}
// Scroll the conversation list column to load more threads.
const firstConvo = document.querySelector(SELECTORS.conversation);
const scroller = firstConvo ? firstConvo.closest('[data-testid="primaryColumn"]') : null;
if (scroller) scroller.scrollTop = scroller.scrollHeight;
else window.scrollTo(0, document.body.scrollHeight);
scrollRounds++;
await sleep(CONFIG.scrollDelay);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 AUTO REPLY DMs - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Replied: ${String(stats.replied).padEnd(32)}║
║ ⏭️ Skipped (read): ${String(stats.skippedRead).padEnd(32)}║
║ 🚫 No composer: ${String(stats.noInput).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("auto-reply-mentions", function(){
(async function autoReplyMentions() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Reply templates, used in rotation (one per mention). REQUIRED.
// Keep them generic and friendly, or personalise per your voice.
replyTemplates: [
'Thanks for the mention! 🙏',
'Appreciate you tagging me 🚀',
'Thanks for reaching out!',
'Glad you brought this up 💡'
],
// Maximum number of mentions to reply to this run
maxReplies: 8,
// Skip mentions you have already replied to in a previous run
// (tracked in this browser's localStorage)
skipIfAlreadyReplied: true,
// Append a random emoji to each reply for variety
addRandomEmoji: false,
// Minimum delay between replies (ms)
minDelay: 3000,
// Maximum delay between replies (ms)
maxDelay: 6000,
// Maximum scroll attempts to find more mentions
maxScrollAttempts: 20,
// Stop if no new mentions appear after this many consecutive scrolls
noNewMentionsThreshold: 4
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
replyButton: '[data-testid="reply"]',
composer: '[data-testid="tweetTextarea_0"]',
postButton: '[data-testid="tweetButton"]',
postButtonInline: '[data-testid="tweetButtonInline"]',
userName: '[data-testid="User-Name"] a[href^="/"]'
};
const STORAGE_KEY = 'xactions_replied_mentions';
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const emojis = ['🔥', '💯', '⚡', '🚀', '👏', '💡', '✨', '🙌', '💪', '🎯'];
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
const scrollDown = () => window.scrollBy(0, window.innerHeight * 0.7);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} replied`)
};
const loadReplied = () => {
try {
return new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'));
} catch {
return new Set();
}
};
const saveReplied = (set) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...set]));
} catch {
// localStorage may be unavailable in some contexts; tracking is best-effort.
}
};
const getTweetIdentifier = (tweet) => {
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const links = tweet.querySelectorAll('a[href*="/status/"]');
for (const link of links) {
const match = link.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
return null;
};
const getAuthor = (tweet) => {
const el = tweet.querySelector(SELECTORS.userName);
return el?.getAttribute('href')?.replace(/^\//, '').split('/')[0] || 'user';
};
// Type text into the composer. X uses a contenteditable Draft.js editor, so
// execCommand('insertText') is the reliable way to enter characters and fire
// the input events X listens for.
const typeInto = async (box, text) => {
box.focus();
await sleep(200);
document.execCommand('insertText', false, text);
await sleep(400);
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
replied: 0,
skippedAlready: 0,
skippedNoReplyButton: 0,
errors: 0
};
let templateIndex = 0;
const nextTemplate = () => {
let text = CONFIG.replyTemplates[templateIndex % CONFIG.replyTemplates.length];
templateIndex++;
if (CONFIG.addRandomEmoji) text += ' ' + pick(emojis);
return text;
};
// Stop switch: run window.stopAutoReplyMentions() to abort after the current reply.
let stopped = false;
window.stopAutoReplyMentions = () => {
stopped = true;
log.warning('Stop requested. Finishing the current reply, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 💬 AUTO REPLY MENTIONS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn (do not redirect) if not on the mentions timeline.
if (!/\/notifications\/mentions/.test(window.location.pathname)) {
log.warning('Go to x.com/notifications/mentions, then run this again.');
return;
}
// Config guard: templates are required and must be non-empty strings.
const templates = (CONFIG.replyTemplates || []).map(t => (t || '').trim()).filter(Boolean);
if (templates.length === 0) {
log.error('CONFIG.replyTemplates is empty. Add at least one reply line and re-run.');
return;
}
CONFIG.replyTemplates = templates;
const replied = CONFIG.skipIfAlreadyReplied ? loadReplied() : new Set();
const processedThisRun = new Set();
log.info(`Max replies: ${CONFIG.maxReplies}`);
log.info(`Templates: ${CONFIG.replyTemplates.length} | Skip already replied: ${CONFIG.skipIfAlreadyReplied}`);
log.info('To stop early: window.stopAutoReplyMentions()');
if (document.querySelector(SELECTORS.tweet) === null) {
log.warning('No mentions visible yet. Wait for the timeline to load, then re-run.');
return;
}
let scrollAttempts = 0;
let noNewMentionsCount = 0;
while (!stopped && stats.replied < CONFIG.maxReplies && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewMention = false;
for (const tweet of tweets) {
if (stopped || stats.replied >= CONFIG.maxReplies) break;
const tweetId = getTweetIdentifier(tweet);
if (!tweetId || processedThisRun.has(tweetId)) continue;
processedThisRun.add(tweetId);
foundNewMention = true;
if (CONFIG.skipIfAlreadyReplied && replied.has(tweetId)) {
stats.skippedAlready++;
continue;
}
const author = getAuthor(tweet);
const preview = (tweet.querySelector(SELECTORS.tweetText)?.textContent || '').substring(0, 50).replace(/\n/g, ' ');
try {
const replyButton = tweet.querySelector(SELECTORS.replyButton);
if (!replyButton) {
stats.skippedNoReplyButton++;
continue;
}
replyButton.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
replyButton.click();
await sleep(1500);
const box = document.querySelector(SELECTORS.composer);
if (!box) {
log.warning(`Reply composer did not open for @${author}. Skipping.`);
document.body.click();
await sleep(600);
continue;
}
const replyText = nextTemplate();
await typeInto(box, replyText);
const postButton = document.querySelector(SELECTORS.postButtonInline) ||
document.querySelector(SELECTORS.postButton);
if (postButton && postButton.getAttribute('aria-disabled') !== 'true') {
postButton.click();
stats.replied++;
replied.add(tweetId);
if (CONFIG.skipIfAlreadyReplied) saveReplied(replied);
log.success(`Replied #${stats.replied} to @${author} ("${preview}...") -> "${replyText}"`);
log.progress(stats.replied, CONFIG.maxReplies);
await sleep(1500);
await randomDelay();
} else {
log.warning(`Post button unavailable for @${author}. Closing composer.`);
document.body.click();
await sleep(600);
}
} catch (error) {
log.error(`Error replying to @${author}: ${error.message}`);
stats.errors++;
document.body.click();
await sleep(800);
}
}
if (!foundNewMention) {
noNewMentionsCount++;
if (noNewMentionsCount >= CONFIG.noNewMentionsThreshold) {
log.warning('No new mentions found after multiple scrolls. Stopping.');
break;
}
} else {
noNewMentionsCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Scrolling for more mentions... (attempt ${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 AUTO REPLY MENTIONS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 💬 Replied: ${String(stats.replied).padEnd(29)}║
║ ⏭️ Skipped (already): ${String(stats.skippedAlready).padEnd(29)}║
║ 🚫 No Reply Button: ${String(stats.skippedNoReplyButton).padEnd(29)}║
║ ❌ Errors: ${String(stats.errors).padEnd(29)}║
║ 📜 Scroll Attempts: ${String(scrollAttempts).padEnd(29)}║
╚══════════════════════════════════════════════════════════╝
`);
if (stats.replied === 0) {
log.warning('No replies sent. You may have already replied to everything, or no mentions loaded.');
}
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("auto-repost", function(){
(async function autoRepost() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of posts to repost this run
maxReposts: 10,
// Skip promoted/ad posts
skipAds: true,
// Skip reply posts (only repost top-level posts)
skipReplies: true,
// Only repost posts that contain an image or video
onlyWithMedia: false,
// Only repost posts whose text contains AT LEAST ONE of these
// (case-insensitive). Empty array = no keyword filter (repost everything).
mustContainKeywords: [],
// Minimum delay between reposts (ms)
minDelay: 1500,
// Maximum delay between reposts (ms)
maxDelay: 4000,
// Maximum scroll attempts to find new posts
maxScrollAttempts: 25,
// Stop if no new posts appear after this many consecutive scrolls
noNewPostsThreshold: 5
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
retweetButton: '[data-testid="retweet"]',
unretweetButton: '[data-testid="unretweet"]',
retweetConfirm: '[data-testid="retweetConfirm"]',
socialContext: '[data-testid="socialContext"]',
promotedLabel: '[data-testid="placementTracking"]',
media: '[data-testid="tweetPhoto"], [data-testid="videoPlayer"], [data-testid="videoComponent"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => window.scrollBy(0, window.innerHeight * 0.7);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} reposted`)
};
const isReply = (tweet) => {
if (tweet.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(tweet.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
};
const isAd = (tweet) => {
if (tweet.querySelector(SELECTORS.promotedLabel) !== null) return true;
return Array.from(tweet.querySelectorAll('span')).some(el => {
const t = el.textContent.trim();
return t === 'Ad' || t === 'Promoted';
});
};
const hasMedia = (tweet) => tweet.querySelector(SELECTORS.media) !== null;
const matchesKeywords = (text) => {
if (CONFIG.mustContainKeywords.length === 0) return true;
const lower = text.toLowerCase();
return CONFIG.mustContainKeywords.some(kw => lower.includes(kw.toLowerCase()));
};
const getTweetIdentifier = (tweet) => {
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const links = tweet.querySelectorAll('a[href*="/status/"]');
for (const link of links) {
const match = link.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
return text.substring(0, 100);
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
reposted: 0,
skippedReplies: 0,
skippedAds: 0,
skippedNoMedia: 0,
skippedNoKeyword: 0,
alreadyReposted: 0,
errors: 0
};
const processedTweets = new Set();
// Stop switch: run window.stopAutoRepost() from the console to abort the
// loop after the post currently being processed.
let stopped = false;
window.stopAutoRepost = () => {
stopped = true;
log.warning('Stop requested. Finishing the current post, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔄 AUTO REPOST - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: reposting reads the rendered timeline. Warn (do not redirect)
// if there is no post feed on screen.
const href = window.location.href;
const looksLikeFeed = /x\.com|twitter\.com/.test(href);
if (!looksLikeFeed) {
log.warning('Open x.com on a search, hashtag, or profile page, then run this again.');
return;
}
if (document.querySelector(SELECTORS.tweet) === null) {
log.warning('No posts visible yet. Go to a search/hashtag results page or a profile, wait for posts to load, then re-run.');
return;
}
log.info(`Max reposts: ${CONFIG.maxReposts}`);
log.info(`Skip ads: ${CONFIG.skipAds} | Skip replies: ${CONFIG.skipReplies} | Only media: ${CONFIG.onlyWithMedia}`);
log.info(`Keywords: ${CONFIG.mustContainKeywords.length ? CONFIG.mustContainKeywords.join(', ') : 'none (repost all)'}`);
log.info('To stop early: window.stopAutoRepost()');
let scrollAttempts = 0;
let noNewPostsCount = 0;
while (!stopped && stats.reposted < CONFIG.maxReposts && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewPost = false;
for (const tweet of tweets) {
if (stopped || stats.reposted >= CONFIG.maxReposts) break;
const tweetId = getTweetIdentifier(tweet);
if (processedTweets.has(tweetId)) continue;
processedTweets.add(tweetId);
foundNewPost = true;
try {
if (CONFIG.skipAds && isAd(tweet)) {
stats.skippedAds++;
continue;
}
if (CONFIG.skipReplies && isReply(tweet)) {
stats.skippedReplies++;
continue;
}
if (CONFIG.onlyWithMedia && !hasMedia(tweet)) {
stats.skippedNoMedia++;
continue;
}
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
if (!matchesKeywords(text)) {
stats.skippedNoKeyword++;
continue;
}
// Already reposted? The button flips to unretweet.
if (tweet.querySelector(SELECTORS.unretweetButton)) {
stats.alreadyReposted++;
continue;
}
const retweetButton = tweet.querySelector(SELECTORS.retweetButton);
if (!retweetButton) continue;
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
retweetButton.click();
await sleep(800);
const confirm = document.querySelector(SELECTORS.retweetConfirm);
if (confirm) {
confirm.click();
stats.reposted++;
const preview = text.substring(0, 50).replace(/\n/g, ' ') || '(no text)';
log.success(`Reposted #${stats.reposted}: "${preview}..."`);
log.progress(stats.reposted, CONFIG.maxReposts);
await randomDelay();
} else {
// Menu did not open as expected (e.g. quote-only). Close it and move on.
document.body.click();
await sleep(400);
}
} catch (error) {
log.error(`Error processing post: ${error.message}`);
stats.errors++;
document.body.click();
await sleep(600);
}
}
if (!foundNewPost) {
noNewPostsCount++;
if (noNewPostsCount >= CONFIG.noNewPostsThreshold) {
log.warning('No new posts found after multiple scrolls. Stopping.');
break;
}
} else {
noNewPostsCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Scrolling for more posts... (attempt ${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
// ============================================
// 📊 SUMMARY
// ============================================
const totalSkipped = stats.skippedReplies + stats.skippedAds + stats.skippedNoMedia + stats.skippedNoKeyword;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 AUTO REPOST - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🔄 Total Reposted: ${String(stats.reposted).padEnd(32)}║
║ ⏭️ Total Skipped: ${String(totalSkipped).padEnd(32)}║
║ └─ Replies: ${String(stats.skippedReplies).padEnd(32)}║
║ └─ Ads: ${String(stats.skippedAds).padEnd(32)}║
║ └─ No Media: ${String(stats.skippedNoMedia).padEnd(32)}║
║ └─ No Keyword: ${String(stats.skippedNoKeyword).padEnd(32)}║
║ 🔁 Already Reposted: ${String(stats.alreadyReposted).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
║ 📜 Scroll Attempts: ${String(scrollAttempts).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
if (stats.reposted === 0) {
log.warning('Nothing reposted. Loosen your filters (keywords/media) or scroll a busier feed.');
}
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("backup-account", function(){
var CONFIG = {
// How many items to collect per category
maxTweets: 100,
maxLikes: 100,
maxBookmarks: 100,
maxFollowing: 500,
maxFollowers: 500,
// Delay between scroll actions (ms)
scrollDelay: 2000,
// Auto-download backup when complete
autoDownload: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function backupAccount() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $tweet = '[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
const $userCell = '[data-testid="UserCell"]';
const $userName = '[data-testid="User-Name"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 💾 FULL ACCOUNT BACKUP ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Storage for backup data
const backupData = {
meta: {
createdAt: new Date().toISOString(),
source: 'XActions Backup Tool',
version: '1.0.0'
},
profile: null,
tweets: [],
likes: [],
bookmarks: [],
following: [],
followers: []
};
// Helper to extract tweet data
const extractTweetData = (tweetEl) => {
const textEl = tweetEl.querySelector($tweetText);
const timeEl = tweetEl.querySelector('time');
// The timestamp's enclosing anchor is the tweet's own permalink; the first
// /status/ link in the article can belong to a quoted tweet
const linkEl = (timeEl && timeEl.closest('a[href*="/status/"]')) ||
tweetEl.querySelector('a[href*="/status/"]');
const userLink = tweetEl.querySelector('[data-testid="User-Name"] a[href^="/"]') ||
tweetEl.querySelector('a[href^="/"]');
return {
text: textEl?.textContent || '',
url: linkEl?.href || '',
tweetId: linkEl?.href?.match(/status\/(\d+)/)?.[1] || '',
timestamp: timeEl?.dateTime || '',
username: userLink?.getAttribute('href')?.replace('/', '')?.split('/')[0] || ''
};
};
// Helper to extract user data
const extractUserData = (userEl) => {
const nameEl = userEl.querySelector($userName);
const linkEl = userEl.querySelector('a[href^="/"]');
const bioEl = userEl.querySelector('[dir="auto"]:not([data-testid])');
return {
name: nameEl?.textContent?.split('@')[0]?.trim() || '',
username: linkEl?.getAttribute('href')?.replace('/', '')?.split('/')[0] || '',
bio: bioEl?.textContent || '',
url: linkEl?.href || ''
};
};
// Scroll and collect items
const scrollAndCollect = async (selector, extractor, maxItems, itemName) => {
const items = new Map();
let lastCount = 0;
let noNewItems = 0;
console.log(`📥 Collecting ${itemName}...`);
while (items.size < maxItems && noNewItems < 5) {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
try {
const data = extractor(el);
const key = data.tweetId || data.username || data.url || JSON.stringify(data);
if (key && !items.has(key)) {
items.set(key, data);
}
} catch (e) {
// Skip invalid elements
}
});
if (items.size === lastCount) {
noNewItems++;
} else {
noNewItems = 0;
lastCount = items.size;
}
console.log(` 📊 ${items.size} ${itemName} collected...`);
window.scrollBy(0, window.innerHeight);
await sleep(CONFIG.scrollDelay);
}
return Array.from(items.values());
};
// Download backup file
const downloadBackup = (data) => {
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `xactions-backup-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('📁 Backup file downloaded!');
};
// Create XActions backup interface
window.XActions = window.XActions || {};
window.XActions.Backup = {
data: backupData,
// Backup tweets from current page
tweets: async () => {
console.log('🐦 Backing up tweets...');
console.log('📍 Make sure you are on your profile page (Posts tab)');
await sleep(1000);
backupData.tweets = await scrollAndCollect($tweet, extractTweetData, CONFIG.maxTweets, 'tweets');
console.log(`✅ Collected ${backupData.tweets.length} tweets`);
return backupData.tweets;
},
// Backup likes
likes: async () => {
console.log('❤️ Backing up likes...');
console.log('📍 Navigate to your Likes tab first: https://x.com/YOUR_USERNAME/likes');
await sleep(1000);
backupData.likes = await scrollAndCollect($tweet, extractTweetData, CONFIG.maxLikes, 'likes');
console.log(`✅ Collected ${backupData.likes.length} likes`);
return backupData.likes;
},
// Backup bookmarks
bookmarks: async () => {
console.log('🔖 Backing up bookmarks...');
console.log('📍 Navigate to Bookmarks: https://x.com/i/bookmarks');
await sleep(1000);
backupData.bookmarks = await scrollAndCollect($tweet, extractTweetData, CONFIG.maxBookmarks, 'bookmarks');
console.log(`✅ Collected ${backupData.bookmarks.length} bookmarks`);
return backupData.bookmarks;
},
// Backup following list
following: async () => {
console.log('👥 Backing up following list...');
console.log('📍 Navigate to Following: https://x.com/YOUR_USERNAME/following');
await sleep(1000);
backupData.following = await scrollAndCollect($userCell, extractUserData, CONFIG.maxFollowing, 'following');
console.log(`✅ Collected ${backupData.following.length} following`);
return backupData.following;
},
// Backup followers list
followers: async () => {
console.log('👥 Backing up followers list...');
console.log('📍 Navigate to Followers: https://x.com/YOUR_USERNAME/followers');
await sleep(1000);
backupData.followers = await scrollAndCollect($userCell, extractUserData, CONFIG.maxFollowers, 'followers');
console.log(`✅ Collected ${backupData.followers.length} followers`);
return backupData.followers;
},
// Get current profile info
profile: () => {
const name = document.querySelector('[data-testid="UserName"]')?.textContent || '';
const bio = document.querySelector('[data-testid="UserDescription"]')?.textContent || '';
const location = document.querySelector('[data-testid="UserLocation"]')?.textContent || '';
const website = document.querySelector('[data-testid="UserUrl"]')?.textContent || '';
const joinDate = document.querySelector('[data-testid="UserJoinDate"]')?.textContent || '';
backupData.profile = {
name,
bio,
location,
website,
joinDate,
capturedAt: new Date().toISOString()
};
console.log('📋 Profile info captured:', backupData.profile);
return backupData.profile;
},
// Download current backup
download: () => {
backupData.meta.downloadedAt = new Date().toISOString();
downloadBackup(backupData);
},
// Show summary
summary: () => {
console.log('');
console.log('📊 BACKUP SUMMARY:');
console.log(` 🐦 Tweets: ${backupData.tweets.length}`);
console.log(` ❤️ Likes: ${backupData.likes.length}`);
console.log(` 🔖 Bookmarks: ${backupData.bookmarks.length}`);
console.log(` 👥 Following: ${backupData.following.length}`);
console.log(` 👥 Followers: ${backupData.followers.length}`);
console.log(` 📋 Profile: ${backupData.profile ? 'Yes' : 'No'}`);
console.log('');
},
// Full backup (run all)
full: async () => {
console.log('🚀 Starting full backup...');
console.log('');
console.log('⚠️ This is a guided process. Follow the instructions.');
console.log('');
// Profile
window.XActions.Backup.profile();
console.log('');
console.log('📋 NEXT STEPS (run each command after navigating):');
console.log('');
console.log('1. Stay on profile → XActions.Backup.tweets()');
console.log('2. Go to Likes tab → XActions.Backup.likes()');
console.log('3. Go to Bookmarks → XActions.Backup.bookmarks()');
console.log('4. Go to Following → XActions.Backup.following()');
console.log('5. Go to Followers → XActions.Backup.followers()');
console.log('6. When done → XActions.Backup.download()');
console.log('');
},
// Help
help: () => {
console.log('');
console.log('📋 BACKUP COMMANDS:');
console.log('');
console.log(' XActions.Backup.full() - Start guided backup');
console.log(' XActions.Backup.tweets() - Backup tweets');
console.log(' XActions.Backup.likes() - Backup likes');
console.log(' XActions.Backup.bookmarks() - Backup bookmarks');
console.log(' XActions.Backup.following() - Backup following');
console.log(' XActions.Backup.followers() - Backup followers');
console.log(' XActions.Backup.profile() - Capture profile info');
console.log(' XActions.Backup.summary() - Show backup summary');
console.log(' XActions.Backup.download() - Download backup file');
console.log('');
}
};
console.log('✅ Account Backup Tool loaded!');
console.log('');
console.log('📋 QUICK START:');
console.log(' Run XActions.Backup.full() for guided backup');
console.log(' Run XActions.Backup.help() for all commands');
console.log('');
})();
});
register("best-time-to-post", function(){
var CONFIG = {
// Number of posts to analyze
maxPosts: 100,
// Delay between scrolls (ms)
scrollDelay: 1500,
// Maximum scroll attempts
maxScrolls: 50,
// Retry when no new posts found
maxRetries: 3
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function bestTimeToPost() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ ⏰ XActions — Best Time To Post ║
║ Analyze when your audience is most active ║
╚══════════════════════════════════════════════════════════════╝
`);
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
const $like = '[data-testid="like"], [data-testid="unlike"]';
const $retweet = '[data-testid="retweet"], [data-testid="unretweet"]';
const $reply = '[data-testid="reply"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '').trim();
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
const username = window.location.pathname.match(/^\/([^\/]+)/)?.[1];
if (!username || ['home', 'explore', 'notifications', 'messages', 'i'].includes(username)) {
console.error('❌ Please navigate to a profile page first!');
return;
}
console.log(`📊 Analyzing posting patterns for @${username}\n`);
console.log('🔄 Scrolling to collect posts...\n');
const posts = new Map();
let retries = 0;
let scrollCount = 0;
while (posts.size < CONFIG.maxPosts && scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = posts.size;
document.querySelectorAll($tweet).forEach(tweet => {
// Get tweet ID
const timeLink = tweet.querySelector('a[href*="/status/"] time')?.closest('a');
const tweetUrl = timeLink?.getAttribute('href') || '';
const tweetId = tweetUrl.split('/status/')[1]?.split('?')[0];
if (!tweetId || posts.has(tweetId)) return;
// Get timestamp
const timeEl = tweet.querySelector('time');
const timestamp = timeEl?.getAttribute('datetime');
if (!timestamp) return;
// Get engagement
const likeBtn = tweet.querySelector($like);
const retweetBtn = tweet.querySelector($retweet);
const replyBtn = tweet.querySelector($reply);
const likes = parseCount(likeBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const retweets = parseCount(retweetBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const replies = parseCount(replyBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const date = new Date(timestamp);
posts.set(tweetId, {
id: tweetId,
timestamp,
hour: date.getHours(),
dayOfWeek: date.getDay(),
engagement: likes + retweets + replies,
likes,
retweets,
replies
});
});
if (posts.size === prevSize) retries++;
else retries = 0;
console.log(` Found ${posts.size} posts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
const postsArray = Array.from(posts.values());
if (postsArray.length < 10) {
console.error('❌ Not enough posts to analyze. Need at least 10 posts.');
return;
}
console.log(`\n✅ Analyzed ${postsArray.length} posts\n`);
// Analyze by hour
const hourStats = {};
for (let h = 0; h < 24; h++) {
hourStats[h] = { posts: 0, totalEngagement: 0 };
}
postsArray.forEach(p => {
hourStats[p.hour].posts++;
hourStats[p.hour].totalEngagement += p.engagement;
});
// Calculate average engagement per hour
const hourlyAvg = Object.entries(hourStats)
.filter(([_, data]) => data.posts > 0)
.map(([hour, data]) => ({
hour: parseInt(hour),
posts: data.posts,
avgEngagement: data.totalEngagement / data.posts
}))
.sort((a, b) => b.avgEngagement - a.avgEngagement);
// Analyze by day of week
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayStats = {};
for (let d = 0; d < 7; d++) {
dayStats[d] = { posts: 0, totalEngagement: 0 };
}
postsArray.forEach(p => {
dayStats[p.dayOfWeek].posts++;
dayStats[p.dayOfWeek].totalEngagement += p.engagement;
});
const dailyAvg = Object.entries(dayStats)
.filter(([_, data]) => data.posts > 0)
.map(([day, data]) => ({
day: parseInt(day),
dayName: dayNames[parseInt(day)],
posts: data.posts,
avgEngagement: data.totalEngagement / data.posts
}))
.sort((a, b) => b.avgEngagement - a.avgEngagement);
// Format hour for display
const formatHour = (h) => {
const ampm = h >= 12 ? 'PM' : 'AM';
const hour12 = h % 12 || 12;
return `${hour12}:00 ${ampm}`;
};
const formatNum = (n) => {
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return Math.round(n).toString();
};
// Output results
console.log('═'.repeat(60));
console.log('⏰ BEST TIMES TO POST');
console.log('═'.repeat(60));
console.log('\n🕐 BY HOUR (Your Local Time):');
console.log('─'.repeat(50));
hourlyAvg.slice(0, 8).forEach((h, i) => {
const bar = '█'.repeat(Math.round(h.avgEngagement / (hourlyAvg[0].avgEngagement || 1) * 20));
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : ' ';
console.log(`${medal} ${formatHour(h.hour).padEnd(10)} ${bar} ${formatNum(h.avgEngagement)} avg (${h.posts} posts)`);
});
console.log('\n📅 BY DAY OF WEEK:');
console.log('─'.repeat(50));
dailyAvg.forEach((d, i) => {
const bar = '█'.repeat(Math.round(d.avgEngagement / (dailyAvg[0].avgEngagement || 1) * 20));
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : ' ';
console.log(`${medal} ${d.dayName.padEnd(12)} ${bar} ${formatNum(d.avgEngagement)} avg (${d.posts} posts)`);
});
// Create heatmap
console.log('\n📊 ENGAGEMENT HEATMAP:');
console.log('─'.repeat(50));
console.log(' ' + ['12a', '3a', '6a', '9a', '12p', '3p', '6p', '9p'].join(' '));
const heatmapData = {};
postsArray.forEach(p => {
const key = `${p.dayOfWeek}-${Math.floor(p.hour / 3)}`;
if (!heatmapData[key]) heatmapData[key] = { count: 0, engagement: 0 };
heatmapData[key].count++;
heatmapData[key].engagement += p.engagement;
});
const heatValues = Object.values(heatmapData).map(d => d.engagement / d.count);
const maxHeat = heatValues.length > 0 ? Math.max(...heatValues) : 0;
const heatChars = [' ', '░', '▒', '▓', '█'];
dayNames.forEach((day, d) => {
let row = day.slice(0, 3).padEnd(4) + ' ';
for (let h = 0; h < 8; h++) {
const key = `${d}-${h}`;
const data = heatmapData[key];
if (data) {
const intensity = maxHeat > 0
? Math.min(4, Math.floor((data.engagement / data.count) / maxHeat * 4))
: 0;
row += heatChars[intensity] + ' ';
} else {
row += '· ';
}
}
console.log(row);
});
console.log('\n' + '═'.repeat(60));
console.log('🎯 RECOMMENDATIONS');
console.log('═'.repeat(60));
const bestHour = hourlyAvg[0];
const bestDay = dailyAvg[0];
const worstHour = hourlyAvg[hourlyAvg.length - 1];
console.log(`\n✅ BEST time to post: ${bestDay.dayName} at ${formatHour(bestHour.hour)}`);
console.log(` Average engagement: ${formatNum(bestHour.avgEngagement)}`);
console.log(`\n❌ WORST time to post: ${formatHour(worstHour.hour)}`);
console.log(` Average engagement: ${formatNum(worstHour.avgEngagement)}`);
// Top 3 time slots
console.log('\n🏆 TOP 3 POSTING TIMES:');
hourlyAvg.slice(0, 3).forEach((h, i) => {
console.log(` ${i + 1}. ${formatHour(h.hour)} — ${formatNum(h.avgEngagement)} avg engagement`);
});
// Save analysis
const storageKey = `xactions_best_time_${username}`;
localStorage.setItem(storageKey, JSON.stringify({
username,
timestamp: new Date().toISOString(),
postCount: postsArray.length,
hourlyAvg,
dailyAvg
}));
console.log('\n' + '═'.repeat(60));
console.log(`💾 Analysis saved to localStorage`);
console.log('═'.repeat(60) + '\n');
})();
});
register("blacklist", function(){
var CONFIG = {
// Storage key
storageKey: 'xactions_blacklist',
// Pre-populate with accounts to avoid
defaultBlacklist: [
// 'spammer123',
// 'botaccount',
],
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(function blacklistManager() {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🚫 BLACKLIST MANAGER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Storage helpers
const getBlacklist = () => {
try {
const data = localStorage.getItem(CONFIG.storageKey);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
};
const saveBlacklist = (list) => {
localStorage.setItem(CONFIG.storageKey, JSON.stringify(list));
};
// Initialize with defaults if empty
const init = () => {
const current = getBlacklist();
if (current.length === 0 && CONFIG.defaultBlacklist.length > 0) {
const defaults = CONFIG.defaultBlacklist.map(u => ({
username: u.replace('@', '').toLowerCase(),
addedAt: Date.now(),
reason: 'default'
}));
saveBlacklist(defaults);
}
};
init();
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Blacklist = {
// Add user to blacklist
add: (username, reason = '') => {
const list = getBlacklist();
const clean = username.replace('@', '').toLowerCase();
if (list.find(u => u.username === clean)) {
console.log(`⚠️ @${clean} is already blacklisted.`);
return false;
}
list.push({
username: clean,
addedAt: Date.now(),
reason: reason || 'manual'
});
saveBlacklist(list);
console.log(`🚫 Added @${clean} to blacklist.`);
return true;
},
// Add multiple users
addBulk: (usernames, reason = 'bulk') => {
let added = 0;
usernames.forEach(u => {
const list = getBlacklist();
const clean = u.replace('@', '').toLowerCase();
if (!list.find(x => x.username === clean)) {
list.push({ username: clean, addedAt: Date.now(), reason });
saveBlacklist(list);
added++;
}
});
console.log(`🚫 Added ${added} users to blacklist.`);
},
// Remove user from blacklist
remove: (username) => {
let list = getBlacklist();
const clean = username.replace('@', '').toLowerCase();
const before = list.length;
list = list.filter(u => u.username !== clean);
if (list.length < before) {
saveBlacklist(list);
console.log(`✅ Removed @${clean} from blacklist.`);
return true;
}
console.log(`⚠️ @${clean} was not in blacklist.`);
return false;
},
// Check if user is blacklisted
includes: (username) => {
const list = getBlacklist();
const clean = username.replace('@', '').toLowerCase();
return list.some(u => u.username === clean);
},
// Alias for includes
has: (username) => window.XActions.Blacklist.includes(username),
// Get all blacklisted users
getAll: () => {
return getBlacklist();
},
// Get just usernames
getUsernames: () => {
return getBlacklist().map(u => u.username);
},
// Count
count: () => {
return getBlacklist().length;
},
// List all
list: () => {
const list = getBlacklist();
console.log('');
console.log('═'.repeat(50));
console.log('🚫 BLACKLISTED USERS');
console.log('═'.repeat(50));
if (list.length === 0) {
console.log('No users blacklisted yet.');
} else {
list.forEach((u, i) => {
const date = new Date(u.addedAt).toLocaleDateString();
console.log(`${i + 1}. @${u.username} (added: ${date}${u.reason ? ', ' + u.reason : ''})`);
});
}
console.log('═'.repeat(50));
console.log(`Total: ${list.length} users`);
console.log('');
},
// Search
search: (query) => {
const list = getBlacklist();
const matches = list.filter(u => u.username.includes(query.toLowerCase()));
console.log(`🔍 Found ${matches.length} matches for "${query}":`);
matches.forEach(u => console.log(` @${u.username}`));
return matches;
},
// Clear all
clear: () => {
if (confirm('⚠️ Clear entire blacklist?')) {
saveBlacklist([]);
console.log('✅ Blacklist cleared.');
}
},
// Export
export: () => {
const list = getBlacklist();
const usernames = list.map(u => u.username);
console.log('📋 Blacklist (copy this):');
console.log(JSON.stringify(usernames));
if (navigator.clipboard) {
navigator.clipboard.writeText(JSON.stringify(usernames))
.then(() => console.log('✅ Copied to clipboard!'))
.catch(() => console.log('⚠️ Clipboard copy failed. Copy the JSON above manually.'));
}
return usernames;
},
// Import
import: (usernamesArray) => {
if (!Array.isArray(usernamesArray)) {
try {
usernamesArray = JSON.parse(usernamesArray);
} catch {
console.error('❌ Invalid format. Provide an array of usernames.');
return;
}
}
window.XActions.Blacklist.addBulk(usernamesArray, 'import');
},
// Block current user (from profile page)
blockCurrentUser: () => {
const urlMatch = window.location.href.match(/x\.com\/([^\/\?]+)/);
if (urlMatch && urlMatch[1]) {
const username = urlMatch[1];
if (!['home', 'explore', 'notifications', 'messages', 'i', 'settings'].includes(username)) {
window.XActions.Blacklist.add(username, 'from profile');
return true;
}
}
console.error('❌ Not on a user profile page.');
return false;
},
// Auto-blacklist users with certain patterns
autoBlacklist: (options = {}) => {
const defaults = {
noProfilePic: true,
noTweets: true,
followRatio: 100, // Following / Followers ratio
};
const opts = { ...defaults, ...options };
console.log('🤖 Auto-blacklist rules:', opts);
console.log('💡 This is a configuration helper. Use with other scripts.');
return opts;
},
// Help
help: () => {
console.log('');
console.log('📋 BLACKLIST COMMANDS:');
console.log('');
console.log(' XActions.Blacklist.add("username")');
console.log(' XActions.Blacklist.add("user", "spam")');
console.log(' XActions.Blacklist.addBulk(["u1", "u2"])');
console.log(' XActions.Blacklist.remove("username")');
console.log(' XActions.Blacklist.has("username")');
console.log(' XActions.Blacklist.list()');
console.log(' XActions.Blacklist.search("pattern")');
console.log(' XActions.Blacklist.count()');
console.log(' XActions.Blacklist.export()');
console.log(' XActions.Blacklist.import([...])');
console.log(' XActions.Blacklist.blockCurrentUser()');
console.log(' XActions.Blacklist.clear()');
console.log('');
}
};
console.log(`🚫 Blacklist Manager loaded! (${getBlacklist().length} users)`);
console.log(' Run XActions.Blacklist.help() for commands.');
console.log('');
})();
});
register("block-bots", function(){
var CONFIG = {
// Scroll settings
scrollDelay: 1500,
maxScrolls: 50,
maxRetries: 3,
// Action delay between blocks
blockDelay: 2000,
// Bot detection thresholds
detection: {
// Following/Followers ratio threshold (e.g., following 5000, followers 50 = ratio 100)
maxFollowingRatio: 50,
// Minimum account age in days
minAccountAgeDays: 30,
// Maximum following count
maxFollowing: 5000,
// Minimum followers count
minFollowers: 5,
// Suspicious bio keywords (case-insensitive)
suspiciousBioKeywords: [
'crypto', 'nft', 'giveaway', 'airdrop', 'free money',
'onlyfans', 'dm for', 'follow back', 'f4f', 'follow4follow',
'bitcoin', 'eth', '$btc', '$eth', 'forex', 'trading signals',
'make money', 'passive income', 'work from home',
'link in bio', 'check bio', 'clickhere', 'sexo', 'sex',
'camgirl', 'hot girl', 'sugar', 'seeking arrangement'
],
// Flag accounts with default profile picture
flagDefaultAvatar: true,
// Flag accounts with no bio
flagNoBio: true,
// Flag accounts with very short usernames + numbers
flagRandomUsername: true
},
// Dry run mode - set to false to actually block
dryRun: true,
// Maximum accounts to block per run
maxBlocks: 50
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function blockBots() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🤖 XActions — Block Bots ║
║ Detect and block bot accounts ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be blocked ║' : '║ 🔴 LIVE MODE - Accounts WILL be blocked ║'}
╚══════════════════════════════════════════════════════════════╝
`);
if (!window.location.pathname.includes('/followers')) {
console.error('❌ Please navigate to your FOLLOWERS page first!');
console.log('👉 Go to: https://x.com/YOUR_USERNAME/followers');
return;
}
const $userCell = '[data-testid="UserCell"]';
// Analyze a user cell for bot indicators
const analyzeUser = (cell) => {
const result = {
username: '',
displayName: '',
bio: '',
isBot: false,
reasons: [],
score: 0
};
// Get username
const usernameLink = cell.querySelector('a[href^="/"]');
if (usernameLink) {
result.username = usernameLink.getAttribute('href').replace('/', '').split('/')[0];
}
// Get display name
const nameEl = cell.querySelector('[dir="ltr"] span');
result.displayName = nameEl?.textContent || result.username;
// Get bio text
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
result.bio = bioEl?.textContent?.toLowerCase() || '';
// Check for suspicious bio keywords
const c = CONFIG.detection;
c.suspiciousBioKeywords.forEach(keyword => {
if (result.bio.includes(keyword.toLowerCase())) {
result.score += 20;
result.reasons.push(`Bio contains "${keyword}"`);
}
});
// Check for no bio
if (c.flagNoBio && !result.bio) {
result.score += 10;
result.reasons.push('No bio');
}
// Check for default avatar
if (c.flagDefaultAvatar) {
const avatar = cell.querySelector('img[src*="default_profile"]');
if (avatar) {
result.score += 15;
result.reasons.push('Default profile picture');
}
}
// Check for random username pattern (short + many numbers)
if (c.flagRandomUsername) {
const numMatch = result.username.match(/\d+/g);
if (numMatch && numMatch.join('').length >= 6) {
result.score += 15;
result.reasons.push('Username has many numbers');
}
}
// Check follower/following stats from cell (if available)
const statsText = cell.textContent;
const followingMatch = statsText.match(/([\d,.]+[KMB]?)\s*Following/i);
const followersMatch = statsText.match(/([\d,.]+[KMB]?)\s*Followers/i);
if (followingMatch && followersMatch) {
const following = parseCount(followingMatch[1]);
const followers = parseCount(followersMatch[1]);
if (following > c.maxFollowing) {
result.score += 15;
result.reasons.push(`Following ${following.toLocaleString()} accounts`);
}
if (followers < c.minFollowers) {
result.score += 10;
result.reasons.push(`Only ${followers} followers`);
}
const ratio = following / (followers || 1);
if (ratio > c.maxFollowingRatio) {
result.score += 25;
result.reasons.push(`High following ratio (${ratio.toFixed(0)}:1)`);
}
}
// Determine if bot based on score
result.isBot = result.score >= 30;
return result;
};
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '');
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
console.log('🔍 Scanning followers for bot patterns...\n');
const scannedUsers = new Set();
const suspectedBots = [];
let retries = 0;
let scrollCount = 0;
while (scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = scannedUsers.size;
document.querySelectorAll($userCell).forEach(cell => {
const analysis = analyzeUser(cell);
if (!analysis.username || scannedUsers.has(analysis.username)) return;
scannedUsers.add(analysis.username);
if (analysis.isBot) {
suspectedBots.push({ ...analysis, element: cell });
}
});
if (scannedUsers.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scannedUsers.size} | Suspected bots: ${suspectedBots.length}`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Scan complete!`);
console.log(` Total scanned: ${scannedUsers.size}`);
console.log(` Suspected bots: ${suspectedBots.length}\n`);
if (suspectedBots.length === 0) {
console.log('🎉 No suspected bots found!');
return;
}
// Sort by score (highest first)
suspectedBots.sort((a, b) => b.score - a.score);
console.log('═'.repeat(60));
console.log('🤖 SUSPECTED BOT ACCOUNTS');
console.log('═'.repeat(60));
suspectedBots.slice(0, 20).forEach((bot, i) => {
console.log(`\n${i + 1}. @${bot.username} (Score: ${bot.score})`);
console.log(` ${bot.displayName}`);
console.log(` Reasons: ${bot.reasons.join(', ')}`);
console.log(` https://x.com/${bot.username}`);
});
if (suspectedBots.length > 20) {
console.log(`\n... and ${suspectedBots.length - 20} more`);
}
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE');
console.log('═'.repeat(60));
console.log('\nTo actually block these accounts:');
console.log('1. Set CONFIG.dryRun = false at the top of the script');
console.log('2. Run the script again');
console.log('\nOr block manually by visiting each profile.');
} else {
console.log('\n' + '═'.repeat(60));
console.log('🔴 BLOCKING ACCOUNTS');
console.log('═'.repeat(60));
const toBlock = suspectedBots.slice(0, CONFIG.maxBlocks);
let blocked = 0;
for (const bot of toBlock) {
console.log(`\n⏳ Blocking @${bot.username}...`);
try {
// Scroll user into view
bot.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
// Click on the user cell to open profile or find block option
const moreButton = bot.element.querySelector('[data-testid="userActions"]');
if (moreButton) {
moreButton.click();
await sleep(500);
// Find block option in dropdown
const blockOption = document.querySelector('[data-testid="block"]');
if (blockOption) {
blockOption.click();
await sleep(500);
// Confirm block
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
confirmBtn.click();
blocked++;
console.log(` ✅ Blocked @${bot.username}`);
}
}
}
await sleep(CONFIG.blockDelay);
} catch (e) {
console.log(` ❌ Failed to block @${bot.username}: ${e.message}`);
}
}
console.log(`\n✅ Blocked ${blocked} accounts`);
}
// Save results
const storageKey = 'xactions_blocked_bots';
const existing = JSON.parse(localStorage.getItem(storageKey) || '[]');
const newEntries = suspectedBots.map(b => ({
username: b.username,
score: b.score,
reasons: b.reasons,
timestamp: new Date().toISOString()
}));
localStorage.setItem(storageKey, JSON.stringify([...existing, ...newEntries].slice(-1000)));
console.log('\n' + '═'.repeat(60));
console.log(`💾 Results saved. Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('═'.repeat(60) + '\n');
})();
});
register("block-by-keywords", function(){
var CONFIG = {
// Keywords to look for in bio (case-insensitive)
blockKeywords: [
'crypto',
'nft',
'giveaway',
'airdrop',
'onlyfans',
'dm for promo',
'follow back',
'f4f'
],
// Scroll settings
scrollDelay: 1500,
maxScrolls: 30,
maxRetries: 3,
// Delay between blocks
blockDelay: 2000,
// Dry run - set to false to actually block
dryRun: true,
// Max accounts to block per run
maxBlocks: 50
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function blockByKeywords() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🚫 XActions — Block By Keywords ║
║ Block users with specific bio keywords ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be blocked ║' : '║ 🔴 LIVE MODE - Accounts WILL be blocked ║'}
╚══════════════════════════════════════════════════════════════╝
`);
if (!window.location.pathname.includes('/followers') && !window.location.pathname.includes('/following')) {
console.error('❌ Please navigate to a followers or following page first!');
return;
}
console.log('🔍 Looking for users with these keywords in bio:');
CONFIG.blockKeywords.forEach(kw => console.log(` • ${kw}`));
console.log('');
const $userCell = '[data-testid="UserCell"]';
const scanned = new Set();
const matches = [];
let retries = 0;
let scrollCount = 0;
while (scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = scanned.size;
document.querySelectorAll($userCell).forEach(cell => {
// Get username
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!username || scanned.has(username)) return;
scanned.add(username);
// Get bio
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const bio = (bioEl?.textContent || '').toLowerCase();
// Check for keywords
const matchedKeywords = CONFIG.blockKeywords.filter(kw =>
bio.includes(kw.toLowerCase())
);
if (matchedKeywords.length > 0) {
matches.push({
username,
bio: bioEl?.textContent || '',
keywords: matchedKeywords,
element: cell
});
}
});
if (scanned.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scanned.size} | Matches: ${matches.length}`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Scan complete!`);
console.log(` Total scanned: ${scanned.size}`);
console.log(` Matching users: ${matches.length}\n`);
if (matches.length === 0) {
console.log('🎉 No users found with those keywords!');
return;
}
console.log('═'.repeat(60));
console.log('🎯 USERS WITH MATCHING KEYWORDS');
console.log('═'.repeat(60));
matches.forEach((m, i) => {
console.log(`\n${i + 1}. @${m.username}`);
console.log(` Keywords: ${m.keywords.join(', ')}`);
console.log(` Bio: "${m.bio.slice(0, 100)}${m.bio.length > 100 ? '...' : ''}"`);
});
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE - No blocks performed');
console.log('Set CONFIG.dryRun = false to actually block');
console.log('═'.repeat(60));
} else {
console.log('\n' + '═'.repeat(60));
console.log('🔴 BLOCKING MATCHING USERS');
console.log('═'.repeat(60));
let blocked = 0;
const toBlock = matches.slice(0, CONFIG.maxBlocks);
for (const user of toBlock) {
try {
user.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
const moreButton = user.element.querySelector('[data-testid="userActions"]');
if (moreButton) {
moreButton.click();
await sleep(500);
const blockOption = document.querySelector('[data-testid="block"]');
if (blockOption) {
blockOption.click();
await sleep(500);
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
confirmBtn.click();
blocked++;
console.log(`✅ Blocked @${user.username}`);
}
}
}
await sleep(CONFIG.blockDelay);
} catch (e) {
console.log(`❌ Failed to block @${user.username}`);
}
}
console.log(`\n✅ Blocked ${blocked}/${toBlock.length} accounts`);
}
// Save log
const storageKey = 'xactions_keyword_blocks';
const log = matches.map(m => ({
username: m.username,
keywords: m.keywords,
timestamp: new Date().toISOString()
}));
localStorage.setItem(storageKey, JSON.stringify(log));
console.log('\n💾 Results saved to localStorage');
})();
});
register("block-by-ratio", function(){
var CONFIG = {
// Maximum following/followers ratio allowed
// e.g., 50 means following 5000, followers 100 = ratio 50:1 = blocked
maxRatio: 50,
// Minimum following count to consider (avoid false positives on new accounts)
minFollowing: 100,
// Minimum followers to not flag (very new accounts)
minFollowers: 5,
// Scroll settings
scrollDelay: 2000,
maxScrolls: 30,
maxRetries: 3,
// Delay between blocks
blockDelay: 2000,
// Dry run mode
dryRun: true,
// Max blocks per run
maxBlocks: 30
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function blockByRatio() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 📊 XActions — Block By Ratio ║
║ Block accounts with suspicious following/follower ratios ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be blocked ║' : '║ 🔴 LIVE MODE - Accounts WILL be blocked ║'}
╚══════════════════════════════════════════════════════════════╝
`);
console.log(`📊 Threshold: Blocking accounts with ratio > ${CONFIG.maxRatio}:1`);
console.log(` (following/followers > ${CONFIG.maxRatio})\n`);
if (!window.location.pathname.includes('/followers')) {
console.error('❌ Please navigate to a FOLLOWERS page first!');
return;
}
const $userCell = '[data-testid="UserCell"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '');
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
console.log('🔍 Scanning followers and checking ratios...\n');
const scanned = new Set();
const flagged = [];
let retries = 0;
let scrollCount = 0;
while (scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = scanned.size;
for (const cell of document.querySelectorAll($userCell)) {
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!username || scanned.has(username)) continue;
scanned.add(username);
// Try to get stats from cell text
const text = cell.textContent;
// Look for following/followers in the cell
const followingMatch = text.match(/([\d,.]+[KMB]?)\s*Following/i);
const followersMatch = text.match(/([\d,.]+[KMB]?)\s*Follower/i);
if (followingMatch && followersMatch) {
const following = parseCount(followingMatch[1]);
const followers = parseCount(followersMatch[1]);
if (following >= CONFIG.minFollowing && followers > 0) {
const ratio = following / followers;
if (ratio > CONFIG.maxRatio || followers < CONFIG.minFollowers) {
flagged.push({
username,
following,
followers,
ratio: ratio.toFixed(1),
element: cell
});
}
}
}
}
if (scanned.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scanned.size} | Flagged: ${flagged.length}`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Scan complete!`);
console.log(` Total scanned: ${scanned.size}`);
console.log(` Flagged accounts: ${flagged.length}\n`);
if (flagged.length === 0) {
console.log('🎉 No accounts with suspicious ratios found!');
return;
}
// Sort by ratio
flagged.sort((a, b) => parseFloat(b.ratio) - parseFloat(a.ratio));
console.log('═'.repeat(60));
console.log('🚨 ACCOUNTS WITH SUSPICIOUS RATIOS');
console.log('═'.repeat(60));
flagged.slice(0, 30).forEach((u, i) => {
console.log(`\n${i + 1}. @${u.username}`);
console.log(` Following: ${u.following.toLocaleString()} | Followers: ${u.followers.toLocaleString()}`);
console.log(` Ratio: ${u.ratio}:1`);
});
if (flagged.length > 30) {
console.log(`\n... and ${flagged.length - 30} more`);
}
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE - No blocks performed');
console.log('Set CONFIG.dryRun = false to actually block');
console.log('═'.repeat(60));
} else {
console.log('\n' + '═'.repeat(60));
console.log('🔴 BLOCKING FLAGGED ACCOUNTS');
console.log('═'.repeat(60));
let blocked = 0;
const toBlock = flagged.slice(0, CONFIG.maxBlocks);
for (const user of toBlock) {
try {
user.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
const moreButton = user.element.querySelector('[data-testid="userActions"]');
if (moreButton) {
moreButton.click();
await sleep(500);
const blockOption = document.querySelector('[data-testid="block"]');
if (blockOption) {
blockOption.click();
await sleep(500);
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
confirmBtn.click();
blocked++;
console.log(`✅ Blocked @${user.username} (ratio: ${user.ratio}:1)`);
}
}
// Close menu if still open
document.body.click();
}
await sleep(CONFIG.blockDelay);
} catch (e) {
console.log(`❌ Failed to block @${user.username}`);
}
}
console.log(`\n✅ Blocked ${blocked}/${toBlock.length} accounts`);
}
// Save log
const storageKey = 'xactions_ratio_blocks';
const log = flagged.map(u => ({
username: u.username,
ratio: u.ratio,
following: u.following,
followers: u.followers,
timestamp: new Date().toISOString()
}));
localStorage.setItem(storageKey, JSON.stringify(log));
console.log('\n💾 Results saved to localStorage');
})();
});
register("block-list-transfer", function(){
(async function blockListTransfer() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// 'export' | 'importBlock' | 'blockFollowersOf'
mode: 'export',
// For 'importBlock': the accounts to block (with or without @).
usernames: [
// 'username1',
// 'username2',
],
// For 'blockFollowersOf': whose followers to block (without @).
targetAccount: '',
// Never block these (with or without @).
whitelist: [],
// Cap on blocks this run (blocking modes only).
maxBlocks: 25,
// SAFETY: preview only. Set to false to actually block. Ignored by 'export'.
dryRun: true,
// Timing (randomized between min and max, per block)
minDelay: 3000,
maxDelay: 6000,
// Scrolling for scraping / follower sweeps
maxScrollRounds: 40,
scrollDelay: 2000,
maxEmptyScrolls: 6
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
userActions: '[data-testid="userActions"]',
blockMenuItem: '[data-testid="block"]',
menuItem: '[role="menuitem"]',
confirm: '[data-testid="confirmationSheetConfirm"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const cleanHandle = (u) => String(u || '').trim().replace(/^@/, '').toLowerCase();
// SPA navigation keeps this script alive; a full window.location load kills it.
const spaNavigate = (path) => {
try {
const target = new URL(path, window.location.href);
if (target.origin === window.location.origin) {
window.history.pushState({}, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
return;
}
} catch (e) { /* fall through */ }
window.location.href = path;
};
const getUsername = (cell) => {
const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
if (!link) return null;
const m = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
if (!m) return null;
if (['home', 'explore', 'notifications', 'messages', 'i', 'search', 'settings'].includes(m[1])) return null;
return m[1];
};
const getDisplayName = (cell) => {
const span = cell.querySelector('a[href^="/"] span');
return span ? span.textContent.trim() : '';
};
const download = (data, filename) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
console.log(`📥 Downloaded: ${filename}`);
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} blocked`)
};
const whiteSet = new Set(CONFIG.whitelist.map(cleanHandle).filter(Boolean));
// ============================================
// 🎯 STATE
// ============================================
const stats = { blocked: 0, scraped: 0, skipped: 0, notFound: 0, errors: 0 };
const blockedList = [];
// Stop switch: run window.stopBlockListTransfer() to abort after the current item.
let stopped = false;
window.stopBlockListTransfer = () => {
stopped = true;
log.warning('Stop requested. Finishing the current account, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🚫 BLOCK LIST TRANSFER - XActions ║
║ 👤 Author: nichxbt 🌐 https://xactions.app ║
${CONFIG.mode === 'export' ? '║ 📤 EXPORT MODE - read-only ║' : (CONFIG.dryRun ? '║ ⚠️ DRY RUN - No accounts will be blocked ║' : '║ 🔴 LIVE MODE - Accounts WILL be blocked ║')}
╚══════════════════════════════════════════════════════════════╝
`);
log.warning('DESTRUCTIVE tool. Blocking affects real accounts. To stop: window.stopBlockListTransfer()');
// ============================================
// 📤 EXPORT: scrape the blocked list
// ============================================
const exportBlocked = async () => {
if (!/\/settings\/blocked/.test(window.location.pathname)) {
log.error('Export needs the blocked-accounts page. Go to https://x.com/settings/blocked_all and re-run.');
return;
}
log.info('Scraping your blocked accounts...');
const found = new Map();
let rounds = 0;
let empty = 0;
while (rounds < CONFIG.maxScrollRounds && empty < CONFIG.maxEmptyScrolls) {
const before = found.size;
for (const cell of document.querySelectorAll(SELECTORS.userCell)) {
const username = getUsername(cell);
if (!username) continue;
const key = username.toLowerCase();
if (found.has(key)) continue;
found.set(key, { username, displayName: getDisplayName(cell) });
}
stats.scraped = found.size;
if (found.size === before) empty++;
else empty = 0;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
rounds++;
}
const list = [...found.values()];
if (list.length === 0) {
log.warning('No blocked accounts found. Your block list may be empty.');
return;
}
log.success(`Scraped ${list.length} blocked accounts.`);
download(
{ exportedAt: new Date().toISOString(), count: list.length, usernames: list.map(u => u.username), accounts: list },
`xactions-block-list-${new Date().toISOString().slice(0, 10)}.json`
);
};
// ============================================
// 🚫 Block one account by profile navigation (importBlock)
// ============================================
const blockByProfile = async (username) => {
spaNavigate(`/${username}`);
await sleep(3000);
let moreBtn = document.querySelector(SELECTORS.userActions);
let waited = 0;
while (!moreBtn && waited < 5000) {
await sleep(500);
waited += 500;
moreBtn = document.querySelector(SELECTORS.userActions);
}
if (!moreBtn) {
log.warning(`Could not open the menu for @${username} (profile may not exist).`);
stats.notFound++;
return false;
}
moreBtn.click();
await sleep(600);
const blockItem = document.querySelector(SELECTORS.blockMenuItem)
|| Array.from(document.querySelectorAll(SELECTORS.menuItem))
.find(i => /\bblock\b/i.test(i.textContent) && !/unblock/i.test(i.textContent));
if (!blockItem) {
document.body.click();
await sleep(300);
log.warning(`No block option for @${username} (already blocked?).`);
stats.notFound++;
return false;
}
blockItem.click();
await sleep(600);
const confirm = document.querySelector(SELECTORS.confirm);
if (confirm) { confirm.click(); await sleep(700); }
return true;
};
// ============================================
// 🚫 Block one account from its follower row (blockFollowersOf)
// ============================================
const blockFromCell = async (cell, username) => {
const moreBtn = cell.querySelector(SELECTORS.userActions) || cell.querySelector('button[aria-label="More"]');
if (!moreBtn) {
log.warning(`No actions (...) button for @${username}.`);
stats.notFound++;
return false;
}
moreBtn.click();
await sleep(600);
const blockItem = document.querySelector(SELECTORS.blockMenuItem)
|| Array.from(document.querySelectorAll(SELECTORS.menuItem))
.find(i => /\bblock\b/i.test(i.textContent) && !/unblock/i.test(i.textContent));
if (!blockItem) {
document.body.click();
await sleep(300);
log.warning(`No block option for @${username}.`);
stats.notFound++;
return false;
}
blockItem.click();
await sleep(600);
const confirm = document.querySelector(SELECTORS.confirm);
if (confirm) { confirm.click(); await sleep(700); }
return true;
};
// ============================================
// 🔁 importBlock
// ============================================
const importBlock = async () => {
const targets = CONFIG.usernames.map(cleanHandle).filter(Boolean);
if (targets.length === 0) {
log.error('CONFIG.usernames is empty. Add handles to block and re-run.');
return;
}
log.info(`Importing ${targets.length} account(s) to block. Max this run: ${CONFIG.maxBlocks}.`);
for (let i = 0; i < targets.length; i++) {
if (stopped) { log.warning('Stopped by user.'); break; }
if (stats.blocked >= CONFIG.maxBlocks) { log.warning(`Reached maxBlocks (${CONFIG.maxBlocks}).`); break; }
const username = targets[i];
if (whiteSet.has(username)) { stats.skipped++; continue; }
try {
if (CONFIG.dryRun) {
log.info(`Would block: @${username}`);
blockedList.push({ username, dryRun: true, timestamp: new Date().toISOString() });
stats.blocked++;
continue;
}
const ok = await blockByProfile(username);
if (ok) {
stats.blocked++;
blockedList.push({ username, timestamp: new Date().toISOString() });
log.success(`Blocked @${username}`);
log.progress(stats.blocked, Math.min(CONFIG.maxBlocks, targets.length));
} else {
stats.errors++;
}
} catch (error) {
log.error(`Error blocking @${username}: ${error.message}`);
stats.errors++;
}
const more = !stopped && stats.blocked < CONFIG.maxBlocks && i < targets.length - 1;
if (more && !CONFIG.dryRun) await randomDelay();
}
};
// ============================================
// 🔁 blockFollowersOf
// ============================================
const blockFollowersOf = async () => {
const target = cleanHandle(CONFIG.targetAccount);
if (!target) {
log.error('CONFIG.targetAccount is empty. Set it to the account whose followers you want to block.');
return;
}
if (!/\/followers/.test(window.location.pathname) || !window.location.pathname.toLowerCase().includes(`/${target}/`)) {
log.error(`Go to https://x.com/${target}/followers and re-run (must be on that account's followers page).`);
return;
}
log.info(`Block-chaining @${target}'s followers. Max this run: ${CONFIG.maxBlocks}.`);
const processed = new Set();
let rounds = 0;
let empty = 0;
while (!stopped && stats.blocked < CONFIG.maxBlocks && rounds < CONFIG.maxScrollRounds && empty < CONFIG.maxEmptyScrolls) {
const cells = document.querySelectorAll(SELECTORS.userCell);
let foundNew = false;
for (const cell of cells) {
if (stopped || stats.blocked >= CONFIG.maxBlocks) break;
const username = getUsername(cell);
if (!username) continue;
const key = username.toLowerCase();
if (processed.has(key)) continue;
processed.add(key);
foundNew = true;
stats.scraped++;
try {
if (whiteSet.has(key) || key === target) { stats.skipped++; continue; }
if (CONFIG.dryRun) {
log.info(`Would block: @${username}`);
blockedList.push({ username, dryRun: true, timestamp: new Date().toISOString() });
stats.blocked++;
continue;
}
const ok = await blockFromCell(cell, username);
if (ok) {
stats.blocked++;
blockedList.push({ username, timestamp: new Date().toISOString() });
log.success(`Blocked @${username}`);
log.progress(stats.blocked, CONFIG.maxBlocks);
await randomDelay();
} else {
stats.errors++;
}
} catch (error) {
log.error(`Error on @${username}: ${error.message}`);
stats.errors++;
document.body.click();
await sleep(300);
}
}
if (!foundNew) empty++;
else empty = 0;
window.scrollTo(0, document.body.scrollHeight);
rounds++;
await sleep(CONFIG.scrollDelay);
}
};
// ============================================
// 🎯 DISPATCH
// ============================================
try {
if (CONFIG.mode === 'export') {
await exportBlocked();
} else if (CONFIG.mode === 'importBlock') {
await importBlock();
} else if (CONFIG.mode === 'blockFollowersOf') {
await blockFollowersOf();
} else {
log.error(`Unknown mode "${CONFIG.mode}". Use 'export', 'importBlock', or 'blockFollowersOf'.`);
}
} catch (error) {
log.error(`Fatal: ${error.message}`);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 BLOCK LIST TRANSFER - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🚫 Blocked: ${String(stats.blocked).padEnd(32)}║
║ 📤 Scraped: ${String(stats.scraped).padEnd(32)}║
║ ⏭️ Skipped: ${String(stats.skipped).padEnd(32)}║
║ ❓ Not found: ${String(stats.notFound).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
║ 🧪 Dry run: ${String(CONFIG.dryRun).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
if ((CONFIG.mode === 'importBlock' || CONFIG.mode === 'blockFollowersOf') && blockedList.length > 0) {
download(
{ mode: CONFIG.mode, dryRun: CONFIG.dryRun, stats, blocked: blockedList, exportedAt: new Date().toISOString() },
`xactions-block-transfer-${CONFIG.dryRun ? 'preview' : 'results'}-${new Date().toISOString().slice(0, 10)}.json`
);
}
if (CONFIG.dryRun && CONFIG.mode !== 'export') {
log.warning('This was a DRY RUN. Set CONFIG.dryRun = false to actually block.');
}
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("bookmark-exporter", function(){
var CONFIG = {
// Maximum bookmarks to export
maxBookmarks: 1000,
// Scroll delay (increase if bookmarks aren't loading)
scrollDelay: 1500,
// Max scroll attempts
maxScrolls: 200,
// Retry when no new bookmarks
maxRetries: 5,
// Export formats
exportJSON: true,
exportCSV: true,
// Copy to clipboard
copyToClipboard: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function bookmarkExporter() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔖 BOOKMARK EXPORTER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify page
if (!window.location.href.includes('/bookmarks')) {
console.error('❌ ERROR: Must be on the Bookmarks page!');
console.log('📍 Go to: https://x.com/i/bookmarks');
return;
}
console.log('🚀 Exporting bookmarks...');
console.log('📜 Auto-scrolling to load all bookmarks...');
console.log('');
const bookmarks = [];
const seenIds = new Set();
let scrolls = 0;
let retries = 0;
let lastCount = 0;
/**
* Extract bookmark data from tweet element
*/
function extractBookmark(tweet) {
try {
// Get tweet ID: the timestamp's enclosing anchor is the tweet's own
// permalink; the first /status/ link can belong to a quoted tweet
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
const link = timeAnchor || tweet.querySelector('a[href*="/status/"]');
if (!link) return null;
const href = link.getAttribute('href');
const match = href.match(/\/status\/(\d+)/);
if (!match) return null;
const tweetId = match[1];
if (seenIds.has(tweetId)) return null;
seenIds.add(tweetId);
// Get author (User-Name block is the real author; a bare first link in
// the article can belong to a reposter's socialContext or a quoted tweet)
const authorLink = tweet.querySelector('[data-testid="User-Name"] a[href^="/"]') ||
tweet.querySelector('a[href^="/"][role="link"]');
const authorUsername = authorLink ? authorLink.getAttribute('href').replace('/', '').split('/')[0] : 'unknown';
// Get display name
const nameSpan = tweet.querySelector('[dir="ltr"] span');
const authorName = nameSpan ? nameSpan.textContent : authorUsername;
// Get text
const textEl = tweet.querySelector($tweetText);
const text = textEl ? textEl.innerText : '';
// Get timestamp
const timeEl = tweet.querySelector('time');
const timestamp = timeEl ? timeEl.getAttribute('datetime') : null;
const displayTime = timeEl ? timeEl.innerText : '';
// Get engagement metrics
const getMetric = (testId) => {
const el = tweet.querySelector(`[data-testid="${testId}"]`);
const span = el?.querySelector('span span');
return span ? span.innerText : '0';
};
// Check for media
const hasImage = tweet.querySelector('[data-testid="tweetPhoto"]') !== null;
const hasVideo = tweet.querySelector('[data-testid="videoPlayer"], [data-testid="videoComponent"]') !== null;
// Get image URLs
const images = [];
tweet.querySelectorAll('[data-testid="tweetPhoto"] img').forEach(img => {
if (img.src && img.src.includes('pbs.twimg.com')) {
images.push(img.src);
}
});
// Extract URLs from text
const urls = [];
tweet.querySelectorAll('a[href^="https://t.co"]').forEach(a => {
const expandedUrl = a.getAttribute('title') || a.textContent;
if (expandedUrl && expandedUrl.startsWith('http')) {
urls.push(expandedUrl);
}
});
return {
id: tweetId,
url: `https://x.com/${authorUsername}/status/${tweetId}`,
author: {
username: authorUsername,
name: authorName
},
text,
timestamp,
displayTime,
metrics: {
replies: getMetric('reply'),
retweets: getMetric('retweet'),
likes: getMetric('like')
},
media: {
hasImage,
hasVideo,
images
},
urls,
exportedAt: new Date().toISOString()
};
} catch (e) {
return null;
}
}
// Scroll and extract
while (bookmarks.length < CONFIG.maxBookmarks && scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const tweets = document.querySelectorAll($tweet);
tweets.forEach(tweet => {
const bookmark = extractBookmark(tweet);
if (bookmark) {
bookmarks.push(bookmark);
}
});
if (bookmarks.length === lastCount) {
retries++;
} else {
retries = 0;
lastCount = bookmarks.length;
}
console.log(`📊 Exported ${bookmarks.length} bookmarks...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log('');
console.log(`✅ Finished exporting: ${bookmarks.length} bookmarks`);
console.log('');
// Build result
const result = {
exportedAt: new Date().toISOString(),
totalBookmarks: bookmarks.length,
bookmarks
};
// Download JSON
if (CONFIG.exportJSON) {
const jsonBlob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const jsonUrl = URL.createObjectURL(jsonBlob);
const jsonLink = document.createElement('a');
jsonLink.href = jsonUrl;
jsonLink.download = `bookmarks_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(jsonLink);
jsonLink.click();
document.body.removeChild(jsonLink);
URL.revokeObjectURL(jsonUrl);
console.log('💾 JSON downloaded!');
}
// Download CSV
if (CONFIG.exportCSV) {
const headers = ['ID', 'Author', 'Username', 'Text', 'Date', 'Likes', 'Retweets', 'Replies', 'Has Image', 'Has Video', 'URL'];
const rows = bookmarks.map(b => [
b.id,
`"${b.author.name.replace(/"/g, '""')}"`,
b.author.username,
`"${b.text.replace(/"/g, '""').replace(/\n/g, ' ')}"`,
`"${b.displayTime.replace(/"/g, '""')}"`,
b.metrics.likes,
b.metrics.retweets,
b.metrics.replies,
b.media.hasImage,
b.media.hasVideo,
b.url
].join(','));
const csv = [headers.join(','), ...rows].join('\n');
const csvBlob = new Blob([csv], { type: 'text/csv' });
const csvUrl = URL.createObjectURL(csvBlob);
const csvLink = document.createElement('a');
csvLink.href = csvUrl;
csvLink.download = `bookmarks_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(csvLink);
csvLink.click();
document.body.removeChild(csvLink);
URL.revokeObjectURL(csvUrl);
console.log('💾 CSV 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.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ EXPORT COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 Total bookmarks: ${bookmarks.length}`);
console.log('');
window.exportedBookmarks = result;
console.log('💡 Access data via: window.exportedBookmarks');
return result;
})();
});
register("bulk-dm", function(){
(async function bulkDM() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Recipients (with or without the leading @)
usernames: [
// 'username1',
// 'username2',
],
// Message template. Placeholders:
// {username} -> the @handle (lowercased, no @)
// {name} -> the recipient's display name (falls back to handle)
message: `Hey {name}! 👋 Thanks for connecting.`,
// Safety limits
maxDMs: 10, // hard cap on DMs sent this run
skipExisting: true, // skip anyone you already have a conversation with
// Timing (randomized between min and max, per DM)
minDelay: 25000, // 25s
maxDelay: 45000 // 45s
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
newMessageBtn: '[data-testid="NewDM_Button"]',
searchInput: '[data-testid="searchPeople"]',
userCell: '[data-testid="UserCell"]',
typeaheadUser: '[data-testid="TypeaheadUser"]',
nextButton: '[data-testid="nextButton"]',
messageInput: '[data-testid="dmComposerTextInput"]',
sendButton: '[data-testid="dmComposerSendButton"]',
conversation: '[data-testid="conversation"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const cleanHandle = (u) => String(u || '').trim().replace(/^@/, '').toLowerCase();
const fillTemplate = (tpl, username, name) =>
tpl.split('{username}').join(username).split('{name}').join(name || username);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} DMs sent`)
};
// ============================================
// 🎯 STATE
// ============================================
const stats = { sent: 0, skipped: 0, notFound: 0, errors: 0 };
// Stop switch: run window.stopBulkDM() from the console to abort after the
// recipient currently being processed.
let stopped = false;
window.stopBulkDM = () => {
stopped = true;
log.warning('Stop requested. Finishing the current DM, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 💬 BULK DM - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
log.warning('Use responsibly. Mass DMing can get your account restricted.');
log.info('To stop early: window.stopBulkDM()');
// Page guard
if (!/\/messages/.test(window.location.pathname)) {
log.error('Not on the Messages page. Go to https://x.com/messages and re-run.');
return stats;
}
const recipients = CONFIG.usernames.map(cleanHandle).filter(Boolean);
if (recipients.length === 0) {
log.error('CONFIG.usernames is empty. Add at least one handle and re-run.');
return stats;
}
log.info(`Recipients: ${recipients.length}`);
log.info(`Max DMs this run: ${CONFIG.maxDMs}`);
// ============================================
// 📬 SEND ONE DM
// ============================================
const sendOne = async (handle) => {
// Open the new-message composer
const newBtn = document.querySelector(SELECTORS.newMessageBtn);
if (!newBtn) {
log.error('New Message button not found. Make sure the Messages inbox is open.');
stats.errors++;
return false;
}
newBtn.click();
await sleep(1500);
const searchInput = document.querySelector(SELECTORS.searchInput);
if (!searchInput) {
log.error('People search box not found.');
stats.errors++;
return false;
}
// Type the handle (contenteditable-safe via execCommand)
searchInput.focus();
document.execCommand('insertText', false, handle);
await sleep(2200);
// Match the exact @handle so a prefix like "john" can't select "johnny".
const cells = document.querySelectorAll(`${SELECTORS.typeaheadUser}, ${SELECTORS.userCell}`);
const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const handleRe = new RegExp('@' + escaped + '(?![a-zA-Z0-9_])', 'i');
let matchCell = null;
for (const cell of cells) {
if (handleRe.test(cell.textContent)) { matchCell = cell; break; }
}
if (!matchCell) {
log.error(`@${handle} not found in search results.`);
stats.notFound++;
return false;
}
// Pull the display name for the {name} placeholder before we navigate away.
let displayName = handle;
const nameSpan = matchCell.querySelector('span');
if (nameSpan && nameSpan.textContent.trim()) displayName = nameSpan.textContent.trim();
matchCell.click();
await sleep(1200);
// Some flows require a "Next" click to open the conversation.
const nextBtn = document.querySelector(SELECTORS.nextButton);
if (nextBtn && !nextBtn.disabled) {
nextBtn.click();
await sleep(1500);
}
const msgInput = document.querySelector(SELECTORS.messageInput);
if (!msgInput) {
log.error('DM text input not found.');
stats.errors++;
return false;
}
const message = fillTemplate(CONFIG.message, handle, displayName);
msgInput.focus();
document.execCommand('insertText', false, message);
await sleep(1000);
const sendBtn = document.querySelector(SELECTORS.sendButton);
if (!sendBtn || sendBtn.getAttribute('aria-disabled') === 'true' || sendBtn.disabled) {
log.error(`Send button unavailable for @${handle}. DMs may be closed for this account.`);
stats.errors++;
return false;
}
sendBtn.click();
await sleep(1500);
log.success(`Sent to @${handle} (${displayName})`);
return true;
};
// ============================================
// 🔁 MAIN LOOP
// ============================================
for (let i = 0; i < recipients.length; i++) {
if (stopped) { log.warning('Stopped by user.'); break; }
if (stats.sent >= CONFIG.maxDMs) {
log.warning(`Reached maxDMs (${CONFIG.maxDMs}). Stopping.`);
break;
}
const handle = recipients[i];
try {
if (CONFIG.skipExisting) {
const openConvos = document.querySelectorAll(SELECTORS.conversation);
const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const existsRe = new RegExp('@' + escaped + '(?![a-zA-Z0-9_])', 'i');
const already = Array.from(openConvos).some(c => existsRe.test(c.textContent));
if (already) {
log.info(`Skipping @${handle} (existing conversation).`);
stats.skipped++;
continue;
}
}
const ok = await sendOne(handle);
if (ok) {
stats.sent++;
log.progress(stats.sent, Math.min(CONFIG.maxDMs, recipients.length));
}
} catch (error) {
log.error(`Error DMing @${handle}: ${error.message}`);
stats.errors++;
}
// Delay before the next recipient (skip the wait after the final one).
const more = !stopped && stats.sent < CONFIG.maxDMs && i < recipients.length - 1;
if (more) {
log.info(`Waiting before the next DM...`);
await randomDelay();
}
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 BULK DM - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Sent: ${String(stats.sent).padEnd(32)}║
║ ⏭️ Skipped (exists): ${String(stats.skipped).padEnd(32)}║
║ ❓ Not found: ${String(stats.notFound).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("clear-all-bookmarks", function(){
var CONFIG = {
// Maximum bookmarks to remove (0 = unlimited)
maxRemove: 0,
// Delay between removals (ms)
removeDelay: 1500,
// Delay after scrolling (ms)
scrollDelay: 2000,
// Max retries when no bookmarks found
maxRetries: 5,
// Show confirmation prompt
confirmStart: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function clearAllBookmarks() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $removeBookmarkBtn = '[data-testid="removeBookmark"]';
const $bookmarkBtn = '[data-testid="bookmark"]'; // Filled bookmark = already bookmarked
const $tweet = '[data-testid="tweet"]';
const $moreBtn = '[data-testid="caret"]';
const $removeFromBookmarks = '[data-testid="removeBookmark"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🗑️ CLEAR ALL BOOKMARKS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on the right page
if (!window.location.href.includes('/bookmarks')) {
console.error('❌ ERROR: You must be on your Bookmarks page!');
console.log('📍 Go to: https://x.com/i/bookmarks');
return;
}
// Confirmation prompt
if (CONFIG.confirmStart) {
const confirmed = confirm(
'⚠️ WARNING: This will remove ALL your bookmarks!\n\n' +
'This action cannot be undone.\n\n' +
'Are you sure you want to continue?'
);
if (!confirmed) {
console.log('❌ Cancelled by user.');
return;
}
}
console.log('🚀 Starting to clear all bookmarks...');
console.log('');
let totalRemoved = 0;
let retries = 0;
// Method 2: Use the share menu
const removeViaMenu = async (tweet) => {
const moreBtn = tweet.querySelector($moreBtn);
if (!moreBtn) return false;
moreBtn.click();
await sleep(500);
// Only click an actual remove-bookmark item; clicking the first menu item
// blindly would trigger an unrelated action (Not interested, Mute, etc.)
const removeBtn = document.querySelector($removeFromBookmarks);
if (removeBtn) {
removeBtn.click();
await sleep(300);
return true;
}
// Close menu if no remove option
document.body.click();
return false;
};
const processBookmarks = async () => {
while (retries < CONFIG.maxRetries) {
// Check limit
if (CONFIG.maxRemove > 0 && totalRemoved >= CONFIG.maxRemove) {
console.log(`🛑 Reached limit of ${CONFIG.maxRemove} removals. Stopping.`);
break;
}
// Check for empty bookmarks
const emptyState = document.querySelector('[data-testid="emptyState"]');
if (emptyState) {
console.log('📭 Bookmarks page is empty!');
break;
}
// Try to remove a bookmark
const tweets = document.querySelectorAll($tweet);
if (tweets.length === 0) {
retries++;
console.log(`🔄 No tweets found. Scrolling... (attempt ${retries}/${CONFIG.maxRetries})`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
continue;
}
// Try first tweet
const tweet = tweets[0];
// Method 1: Look for remove bookmark button
const removeBtn = tweet.querySelector($removeBookmarkBtn);
if (removeBtn) {
try {
removeBtn.click();
totalRemoved++;
retries = 0;
console.log(`🗑️ Removed bookmark ${totalRemoved}${CONFIG.maxRemove > 0 ? '/' + CONFIG.maxRemove : ''}`);
await sleep(CONFIG.removeDelay);
continue;
} catch (e) {
// Try next method
}
}
// Method 2: Use tweet menu
if (await removeViaMenu(tweet)) {
totalRemoved++;
retries = 0;
console.log(`🗑️ Removed bookmark ${totalRemoved} (via menu)`);
await sleep(CONFIG.removeDelay);
continue;
}
// No remove option found, scroll
retries++;
console.log(`🔄 Couldn't find remove option. Scrolling... (attempt ${retries}/${CONFIG.maxRetries})`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
}
// Done
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(`║ 🎉 COMPLETE! Removed ${totalRemoved} bookmarks `);
console.log('╚════════════════════════════════════════════════════════════╝');
};
processBookmarks();
})();
});
register("clear-all-likes", function(){
var CONFIG = {
// Maximum likes to remove (0 = unlimited)
maxUnlikes: 0,
// Delay between unlikes (ms) - higher = safer from rate limits
unlikeDelay: 1500,
// Delay after scrolling (ms)
scrollDelay: 2000,
// Max retries when no likes found
maxRetries: 5,
// Show confirmation prompt
confirmStart: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function clearAllLikes() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $unlikeButton = '[data-testid="unlike"]';
const $tweet = '[data-testid="tweet"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 💔 CLEAR ALL LIKES ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on the right page
if (!window.location.href.includes('/likes')) {
console.error('❌ ERROR: You must be on your Likes page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME/likes');
return;
}
// Confirmation prompt
if (CONFIG.confirmStart) {
const confirmed = confirm(
'⚠️ WARNING: This will unlike ALL your liked tweets!\n\n' +
'This action cannot be undone.\n\n' +
'Are you sure you want to continue?'
);
if (!confirmed) {
console.log('❌ Cancelled by user.');
return;
}
}
console.log('🚀 Starting to clear all likes...');
console.log('');
let totalUnliked = 0;
let retries = 0;
const processLikes = async () => {
while (retries < CONFIG.maxRetries) {
// Check limit
if (CONFIG.maxUnlikes > 0 && totalUnliked >= CONFIG.maxUnlikes) {
console.log(`🛑 Reached limit of ${CONFIG.maxUnlikes} unlikes. Stopping.`);
break;
}
// Find unlike buttons
const unlikeButtons = document.querySelectorAll($unlikeButton);
if (unlikeButtons.length === 0) {
retries++;
console.log(`🔄 No likes found on screen. Scrolling... (attempt ${retries}/${CONFIG.maxRetries})`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
continue;
}
retries = 0; // Reset retries on success
// Click first unlike button
const btn = unlikeButtons[0];
try {
btn.click();
totalUnliked++;
console.log(`💔 Unliked tweet ${totalUnliked}${CONFIG.maxUnlikes > 0 ? '/' + CONFIG.maxUnlikes : ''}`);
await sleep(CONFIG.unlikeDelay);
} catch (e) {
console.warn('⚠️ Error clicking unlike button:', e.message);
await sleep(CONFIG.unlikeDelay);
}
// Scroll periodically to load more
if (totalUnliked % 10 === 0) {
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
}
}
// Done
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(`║ 🎉 COMPLETE! Unliked ${totalUnliked} tweets `);
console.log('╚════════════════════════════════════════════════════════════╝');
if (retries >= CONFIG.maxRetries) {
console.log('💡 No more likes found. Your likes page should be empty!');
}
};
processLikes();
})();
});
register("clear-all-retweets", function(){
var CONFIG = {
// Maximum retweets to undo (0 = unlimited)
maxUndo: 0,
// Delay between unretweets (ms)
unretweetDelay: 2000,
// Delay after scrolling (ms)
scrollDelay: 2500,
// Max retries when no retweets found
maxRetries: 5,
// Show confirmation prompt
confirmStart: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function clearAllRetweets() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $retweetBtn = '[data-testid="retweet"]';
const $unretweetBtn = '[data-testid="unretweet"]';
const $unretweetConfirm = '[data-testid="unretweetConfirm"]';
const $tweet = '[data-testid="tweet"]';
const $retweetIndicator = '[data-testid="socialContext"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔄 CLEAR ALL RETWEETS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Confirmation prompt
if (CONFIG.confirmStart) {
const confirmed = confirm(
'⚠️ WARNING: This will undo ALL your retweets!\n\n' +
'This removes retweets from your timeline.\n\n' +
'Are you sure you want to continue?'
);
if (!confirmed) {
console.log('❌ Cancelled by user.');
return;
}
}
console.log('🚀 Starting to undo all retweets...');
console.log('📍 Looking for retweets on your profile...');
console.log('');
let totalUndone = 0;
let retries = 0;
// Helper to check if tweet is a retweet
const isRetweet = (tweet) => {
// Check for "You reposted" indicator
const socialContext = tweet.querySelector($retweetIndicator);
if (socialContext?.textContent?.toLowerCase()?.includes('repost')) {
return true;
}
// Check for unretweet button (green retweet icon)
const unretweetBtn = tweet.querySelector($unretweetBtn);
return !!unretweetBtn;
};
// Helper to undo a retweet
const undoRetweet = async (tweet) => {
// Find the unretweet button
const unretweetBtn = tweet.querySelector($unretweetBtn);
if (!unretweetBtn) {
return false;
}
try {
// Click unretweet
unretweetBtn.click();
await sleep(500);
// Look for confirmation menu
const confirmBtn = document.querySelector($unretweetConfirm);
if (confirmBtn) {
confirmBtn.click();
await sleep(300);
}
return true;
} catch (e) {
console.warn('⚠️ Error undoing retweet:', e.message);
return false;
}
};
const processRetweets = async () => {
while (retries < CONFIG.maxRetries) {
// Check limit
if (CONFIG.maxUndo > 0 && totalUndone >= CONFIG.maxUndo) {
console.log(`🛑 Reached limit of ${CONFIG.maxUndo} unretweets. Stopping.`);
break;
}
// Find all tweets
const tweets = document.querySelectorAll($tweet);
let foundRetweet = false;
for (const tweet of tweets) {
if (isRetweet(tweet)) {
// Only count a pass as progress when an undo succeeds; otherwise a
// tweet that looks like a retweet but cannot be undone would spin
// this loop forever with no scroll, retry, or delay
if (await undoRetweet(tweet)) {
foundRetweet = true;
totalUndone++;
retries = 0;
console.log(`🔄 Undid retweet ${totalUndone}${CONFIG.maxUndo > 0 ? '/' + CONFIG.maxUndo : ''}`);
await sleep(CONFIG.unretweetDelay);
break; // Start over from top
}
}
}
if (!foundRetweet) {
retries++;
console.log(`🔍 No retweets found on screen. Scrolling... (attempt ${retries}/${CONFIG.maxRetries})`);
// Scroll to load more
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
}
}
// Done
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(`║ 🎉 COMPLETE! Undid ${totalUndone} retweets `);
console.log('╚════════════════════════════════════════════════════════════╝');
if (retries >= CONFIG.maxRetries) {
console.log('💡 No more retweets found on the visible timeline.');
console.log(' If you have more retweets, scroll down and run again.');
}
};
processRetweets();
})();
});
register("comment-by-hashtag", function(){
(async function commentByHashtag() {
'use strict';
// ===========================================================================
// CONFIGURATION - Customize these settings
// ===========================================================================
const CONFIG = {
// Hashtags to search for (without #)
hashtags: ['web3', 'crypto', 'NFT'],
// Comments to randomly pick from
comments: [
'Great point! 🔥',
'This is so true! 💯',
'Interesting perspective!',
'Thanks for sharing this! 🙌',
'Couldn\'t agree more!'
],
// Maximum number of comments to post
maxComments: 10,
// Delay between actions (milliseconds)
minDelay: 3000,
maxDelay: 6000,
// Skip tweets from these usernames
skipUsernames: [],
// Only comment on tweets with minimum engagement
minLikes: 0,
minRetweets: 0
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ===========================================================================
// SELECTORS
// ===========================================================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
replyButton: '[data-testid="reply"]',
tweetTextarea: '[data-testid="tweetTextarea_0"]',
tweetButton: '[data-testid="tweetButton"]',
searchInput: '[data-testid="SearchBox_Search_Input"]',
timeline: '[data-testid="primaryColumn"]'
};
// ===========================================================================
// HELPERS
// ===========================================================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
return sleep(delay);
};
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const log = (msg, type = 'info') => {
const styles = {
info: 'color: #1DA1F2; font-weight: bold;',
success: 'color: #17BF63; font-weight: bold;',
error: 'color: #E0245E; font-weight: bold;',
warn: 'color: #FFAD1F; font-weight: bold;'
};
console.log(`%c[XActions] ${msg}`, styles[type] || styles.info);
};
const getProcessedTweets = () => {
try {
return JSON.parse(sessionStorage.getItem('xactions_commented_tweets') || '[]');
} catch {
return [];
}
};
const markTweetProcessed = (tweetId) => {
const tweets = getProcessedTweets();
if (!tweets.includes(tweetId)) {
tweets.push(tweetId);
sessionStorage.setItem('xactions_commented_tweets', JSON.stringify(tweets));
}
};
const getTweetId = (tweet) => {
// The timestamp's enclosing anchor is the tweet's own permalink; the first
// /status/ link in the article can belong to a quoted tweet
const timeEl = tweet.querySelector('time');
const link = (timeEl && timeEl.closest('a[href*="/status/"]')) ||
tweet.querySelector('a[href*="/status/"]');
if (link) {
const match = link.href.match(/\/status\/(\d+)/);
return match ? match[1] : null;
}
return null;
};
// ===========================================================================
// MAIN FUNCTIONS
// ===========================================================================
const searchHashtag = async (hashtag) => {
// Assigning location.href reloads the page and kills this script, so only
// navigate when not already on the hashtag's search results, and tell the
// user to re-run after the reload (processed IDs persist in sessionStorage)
const onSearch = window.location.pathname === '/search' &&
decodeURIComponent(window.location.search).toLowerCase().includes(`#${hashtag.toLowerCase()}`);
if (onSearch) {
log(`🔍 On search results for #${hashtag}`);
return true;
}
log(`🔍 Navigating to #${hashtag} search. The page will reload; paste and run this script again to start commenting.`, 'warn');
const searchUrl = `https://x.com/search?q=%23${encodeURIComponent(hashtag)}&src=typed_query&f=live`;
window.location.href = searchUrl;
await sleep(3000);
return false;
};
const postComment = async (tweet, comment) => {
try {
// Click reply button
const replyBtn = tweet.querySelector(SELECTORS.replyButton);
if (!replyBtn) {
log('Reply button not found', 'warn');
return false;
}
replyBtn.click();
await sleep(1500);
// Find and fill textarea
const textarea = document.querySelector(SELECTORS.tweetTextarea);
if (!textarea) {
log('Tweet textarea not found', 'warn');
// Close modal if open
document.querySelector('[data-testid="app-bar-close"]')?.click();
return false;
}
// Focus and type
textarea.focus();
await sleep(300);
// Use execCommand for better compatibility
document.execCommand('insertText', false, comment);
await sleep(500);
// Click reply/tweet button
const tweetBtn = document.querySelector(SELECTORS.tweetButton);
if (!tweetBtn || tweetBtn.disabled) {
log('Tweet button not found or disabled', 'warn');
document.querySelector('[data-testid="app-bar-close"]')?.click();
return false;
}
tweetBtn.click();
await sleep(2000);
return true;
} catch (err) {
log(`Error posting comment: ${err.message}`, 'error');
return false;
}
};
const processHashtag = async (hashtag, stats) => {
if (!(await searchHashtag(hashtag))) return false;
await sleep(2000);
// A Set that we also update locally on every mark, not just a one-time
// snapshot: tweets stay in the DOM across scrolls, so a stale snapshot
// would cause the same tweet to be re-commented on every outer iteration
const processedTweets = new Set(getProcessedTweets());
let scrollAttempts = 0;
const maxScrollAttempts = 20;
while (stats.commented < CONFIG.maxComments && scrollAttempts < maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
for (const tweet of tweets) {
if (stats.commented >= CONFIG.maxComments) break;
const tweetId = getTweetId(tweet);
if (!tweetId || processedTweets.has(tweetId)) continue;
// Get tweet text
const textEl = tweet.querySelector(SELECTORS.tweetText);
const tweetText = textEl?.textContent || '';
// Check if tweet contains hashtag
if (!tweetText.toLowerCase().includes(`#${hashtag.toLowerCase()}`)) continue;
// Skip certain usernames
const usernameEl = tweet.querySelector('[data-testid="User-Name"] a');
const username = usernameEl?.href?.split('/').pop();
if (CONFIG.skipUsernames.includes(username)) {
log(`⏭️ Skipping @${username}`, 'warn');
continue;
}
stats.processed++;
// Scroll tweet into view
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(1000);
// Pick random comment
const comment = randomItem(CONFIG.comments);
log(`💬 Commenting on tweet ${tweetId.slice(-6)}...`);
const success = await postComment(tweet, comment);
if (success) {
stats.commented++;
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
log(`✅ Comment ${stats.commented}/${CONFIG.maxComments} posted!`, 'success');
} else {
stats.failed++;
}
await randomDelay();
}
// Scroll for more tweets
window.scrollBy(0, 800);
await sleep(1500);
scrollAttempts++;
}
return true;
};
// ===========================================================================
// EXECUTION
// ===========================================================================
console.clear();
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ 🏷️ XACTIONS - COMMENT BY HASHTAG ║
║ ║
║ Automatically comment on tweets with specific hashtags ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
`);
log('🚀 Starting Comment By Hashtag...', 'info');
log(`📋 Hashtags: ${CONFIG.hashtags.map(h => '#' + h).join(', ')}`);
log(`💬 Max comments: ${CONFIG.maxComments}`);
const stats = {
processed: 0,
commented: 0,
failed: 0,
startTime: Date.now()
};
try {
for (const hashtag of CONFIG.hashtags) {
if (stats.commented >= CONFIG.maxComments) break;
const completed = await processHashtag(hashtag, stats);
if (!completed) break; // Page is navigating; the script must be re-run
}
} catch (err) {
log(`Fatal error: ${err.message}`, 'error');
}
// ===========================================================================
// SUMMARY
// ===========================================================================
const duration = ((Date.now() - stats.startTime) / 1000 / 60).toFixed(1);
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ 📊 COMPLETION SUMMARY ║
╠═══════════════════════════════════════════════════════════════╣
║ ✅ Comments posted: ${String(stats.commented).padEnd(38)}║
║ ❌ Failed: ${String(stats.failed).padEnd(38)}║
║ 📝 Tweets processed: ${String(stats.processed).padEnd(38)}║
║ ⏱️ Duration: ${String(duration + ' minutes').padEnd(38)}║
╚═══════════════════════════════════════════════════════════════╝
`);
log('🎉 Comment By Hashtag completed!', 'success');
})();
});
register("comment-by-location", function(){
(async function commentByLocation() {
'use strict';
// ===========================================================================
// CONFIGURATION - Customize these settings
// ===========================================================================
const CONFIG = {
// Location to search for (city, country, or place)
location: 'New York',
// Optional: geocode for precise location (lat,long,radius)
// Get coordinates from: https://www.latlong.net/
geocode: null, // e.g., '40.7128,-74.0060,25mi'
// Search query to combine with location (optional)
searchQuery: '',
// Comments to randomly pick from
comments: [
'Love seeing posts from this area! 🌍',
'Great content from a great place! 🔥',
'Thanks for sharing! 💯',
'Awesome post! 🙌',
'This is amazing! ✨'
],
// Maximum number of comments to post
maxComments: 10,
// Delay between actions (milliseconds)
minDelay: 3000,
maxDelay: 7000,
// Skip tweets from these usernames
skipUsernames: [],
// Only comment on recent tweets (hours)
maxTweetAge: 24,
// Skip retweets
skipRetweets: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ===========================================================================
// SELECTORS
// ===========================================================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
replyButton: '[data-testid="reply"]',
tweetTextarea: '[data-testid="tweetTextarea_0"]',
tweetButton: '[data-testid="tweetButton"]',
searchInput: '[data-testid="SearchBox_Search_Input"]',
retweet: '[data-testid="socialContext"]',
timestamp: 'time'
};
// ===========================================================================
// HELPERS
// ===========================================================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
return sleep(delay);
};
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
const log = (msg, type = 'info') => {
const styles = {
info: 'color: #1DA1F2; font-weight: bold;',
success: 'color: #17BF63; font-weight: bold;',
error: 'color: #E0245E; font-weight: bold;',
warn: 'color: #FFAD1F; font-weight: bold;'
};
console.log(`%c[XActions] ${msg}`, styles[type] || styles.info);
};
const getProcessedTweets = () => {
try {
return JSON.parse(sessionStorage.getItem('xactions_location_commented') || '[]');
} catch {
return [];
}
};
const markTweetProcessed = (tweetId) => {
const tweets = getProcessedTweets();
if (!tweets.includes(tweetId)) {
tweets.push(tweetId);
sessionStorage.setItem('xactions_location_commented', JSON.stringify(tweets));
}
};
const getTweetId = (tweet) => {
// The timestamp's enclosing anchor is the tweet's own permalink; the first
// /status/ link in the article can belong to a quoted tweet
const timeEl = tweet.querySelector('time');
const link = (timeEl && timeEl.closest('a[href*="/status/"]')) ||
tweet.querySelector('a[href*="/status/"]');
if (link) {
const match = link.href.match(/\/status\/(\d+)/);
return match ? match[1] : null;
}
return null;
};
const isTweetRecent = (tweet) => {
const timeEl = tweet.querySelector(SELECTORS.timestamp);
if (!timeEl) return true;
const tweetTime = new Date(timeEl.getAttribute('datetime'));
const hoursAgo = (Date.now() - tweetTime.getTime()) / (1000 * 60 * 60);
return hoursAgo <= CONFIG.maxTweetAge;
};
const isRetweet = (tweet) => {
// Retweets render socialContext as a link to the reposter's profile;
// pinned posts render it as a plain element. Structural check works on
// any UI language.
const socialContext = tweet.querySelector(SELECTORS.retweet);
return !!socialContext && socialContext.closest('a') !== null;
};
// ===========================================================================
// MAIN FUNCTIONS
// ===========================================================================
const buildSearchUrl = () => {
let query = CONFIG.searchQuery;
if (CONFIG.geocode) {
query += ` geocode:${CONFIG.geocode}`;
} else if (CONFIG.location) {
query += ` near:"${CONFIG.location}"`;
}
const encodedQuery = encodeURIComponent(query.trim());
return `https://x.com/search?q=${encodedQuery}&src=typed_query&f=live`;
};
const navigateToSearch = async () => {
// Assigning location.href reloads the page and kills this script, so only
// navigate when not already on the matching search results, and tell the
// user to re-run after the reload (processed IDs persist in sessionStorage)
const query = CONFIG.geocode ? `geocode:${CONFIG.geocode}` : (CONFIG.location ? `near:"${CONFIG.location}"` : '');
const onSearch = window.location.pathname === '/search' &&
decodeURIComponent(window.location.search).toLowerCase().includes(query.toLowerCase());
if (onSearch) {
log(`🔍 On search results for "${CONFIG.location || CONFIG.geocode}"`);
return true;
}
log(`🔍 Navigating to the location search. The page will reload; paste and run this script again to start commenting.`, 'warn');
const searchUrl = buildSearchUrl();
window.location.href = searchUrl;
await sleep(4000);
return false;
};
const postComment = async (tweet, comment) => {
try {
// Click reply button
const replyBtn = tweet.querySelector(SELECTORS.replyButton);
if (!replyBtn) {
log('Reply button not found', 'warn');
return false;
}
replyBtn.click();
await sleep(1500);
// Find and fill textarea
const textarea = document.querySelector(SELECTORS.tweetTextarea);
if (!textarea) {
log('Tweet textarea not found', 'warn');
document.querySelector('[data-testid="app-bar-close"]')?.click();
return false;
}
// Focus and type
textarea.focus();
await sleep(300);
document.execCommand('insertText', false, comment);
await sleep(500);
// Click reply/tweet button
const tweetBtn = document.querySelector(SELECTORS.tweetButton);
if (!tweetBtn || tweetBtn.disabled) {
log('Tweet button not found or disabled', 'warn');
document.querySelector('[data-testid="app-bar-close"]')?.click();
return false;
}
tweetBtn.click();
await sleep(2000);
return true;
} catch (err) {
log(`Error posting comment: ${err.message}`, 'error');
return false;
}
};
const processTweets = async (stats) => {
// A Set that we also update locally on every mark, not just a one-time
// snapshot: tweets stay in the DOM across scrolls, so a stale snapshot
// would cause the same tweet to be re-processed/re-commented on every
// outer iteration instead of being skipped as already handled
const processedTweets = new Set(getProcessedTweets());
let scrollAttempts = 0;
const maxScrollAttempts = 30;
let noNewTweetsCount = 0;
while (stats.commented < CONFIG.maxComments && scrollAttempts < maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNew = false;
for (const tweet of tweets) {
if (stats.commented >= CONFIG.maxComments) break;
const tweetId = getTweetId(tweet);
if (!tweetId || processedTweets.has(tweetId)) continue;
foundNew = true;
// Skip retweets if configured
if (CONFIG.skipRetweets && isRetweet(tweet)) {
log(`⏭️ Skipping retweet`, 'warn');
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
continue;
}
// Check tweet age
if (!isTweetRecent(tweet)) {
log(`⏭️ Skipping old tweet`, 'warn');
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
continue;
}
// Skip certain usernames
const usernameEl = tweet.querySelector('[data-testid="User-Name"] a');
const username = usernameEl?.href?.split('/').pop();
if (CONFIG.skipUsernames.includes(username)) {
log(`⏭️ Skipping @${username}`, 'warn');
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
continue;
}
stats.processed++;
// Scroll tweet into view
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(1000);
// Pick random comment
const comment = randomItem(CONFIG.comments);
log(`💬 Commenting on tweet from ${username || 'user'}...`);
const success = await postComment(tweet, comment);
if (success) {
stats.commented++;
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
log(`✅ Comment ${stats.commented}/${CONFIG.maxComments} posted!`, 'success');
} else {
stats.failed++;
markTweetProcessed(tweetId);
processedTweets.add(tweetId);
}
await randomDelay();
}
if (!foundNew) {
noNewTweetsCount++;
if (noNewTweetsCount >= 3) {
log('No more new tweets found', 'warn');
break;
}
} else {
noNewTweetsCount = 0;
}
// Scroll for more tweets
window.scrollBy(0, 800);
await sleep(1500);
scrollAttempts++;
}
};
// ===========================================================================
// EXECUTION
// ===========================================================================
console.clear();
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ 📍 XACTIONS - COMMENT BY LOCATION ║
║ ║
║ Automatically comment on tweets from specific locations ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
`);
log('🚀 Starting Comment By Location...', 'info');
log(`📍 Location: ${CONFIG.location}`);
log(`💬 Max comments: ${CONFIG.maxComments}`);
if (CONFIG.geocode) {
log(`🌐 Using geocode: ${CONFIG.geocode}`);
}
const stats = {
processed: 0,
commented: 0,
failed: 0,
startTime: Date.now()
};
try {
if (await navigateToSearch()) {
await processTweets(stats);
}
} catch (err) {
log(`Fatal error: ${err.message}`, 'error');
console.error(err);
}
// ===========================================================================
// SUMMARY
// ===========================================================================
const duration = ((Date.now() - stats.startTime) / 1000 / 60).toFixed(1);
console.log(`
╔═══════════════════════════════════════════════════════════════╗
║ 📊 COMPLETION SUMMARY ║
╠═══════════════════════════════════════════════════════════════╣
║ 📍 Location: ${String(CONFIG.location).padEnd(38)}║
║ ✅ Comments posted: ${String(stats.commented).padEnd(38)}║
║ ❌ Failed: ${String(stats.failed).padEnd(38)}║
║ 📝 Tweets processed: ${String(stats.processed).padEnd(38)}║
║ ⏱️ Duration: ${String(duration + ' minutes').padEnd(38)}║
╚═══════════════════════════════════════════════════════════════╝
`);
log('🎉 Comment By Location completed!', 'success');
})();
});
register("competitor-analysis", function(){
var CONFIG = {
// Number of posts to analyze
maxPosts: 50,
// Delay between scrolls (ms)
scrollDelay: 1500,
// Maximum scroll attempts
maxScrolls: 30,
// Retry when no new posts found
maxRetries: 3
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function competitorAnalysis() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🔎 XActions — Competitor Analysis ║
║ Analyze competitor accounts for insights ║
╚══════════════════════════════════════════════════════════════╝
`);
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
const $like = '[data-testid="like"], [data-testid="unlike"]';
const $retweet = '[data-testid="retweet"], [data-testid="unretweet"]';
const $reply = '[data-testid="reply"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '').trim();
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
const username = window.location.pathname.match(/^\/([^\/]+)/)?.[1];
if (!username || ['home', 'explore', 'notifications', 'messages', 'i'].includes(username)) {
console.error('❌ Please navigate to a profile page first!');
return;
}
// Get profile stats
const getProfileStats = () => {
const stats = { followers: 0, following: 0, name: '', bio: '' };
// Get display name
const nameEl = document.querySelector('[data-testid="UserName"]');
stats.name = nameEl?.textContent?.split('@')[0]?.trim() || username;
// Get bio
const bioEl = document.querySelector('[data-testid="UserDescription"]');
stats.bio = bioEl?.textContent || '';
// Get follower/following counts
document.querySelectorAll('a').forEach(link => {
const href = link.getAttribute('href') || '';
const text = link.textContent || '';
if (href.includes('/followers') || href.includes('/verified_followers')) {
const match = text.match(/([\d,.]+[KMB]?)/);
if (match) stats.followers = parseCount(match[1]);
}
if (href.includes('/following')) {
const match = text.match(/([\d,.]+[KMB]?)/);
if (match) stats.following = parseCount(match[1]);
}
});
return stats;
};
console.log(`🔎 Analyzing @${username}\n`);
const profileStats = getProfileStats();
console.log(`👤 ${profileStats.name}`);
console.log(`📊 Followers: ${profileStats.followers.toLocaleString()} | Following: ${profileStats.following.toLocaleString()}`);
console.log(`📝 Bio: ${profileStats.bio.slice(0, 100)}${profileStats.bio.length > 100 ? '...' : ''}\n`);
console.log('🔄 Scrolling to analyze posts...\n');
const posts = new Map();
let retries = 0;
let scrollCount = 0;
while (posts.size < CONFIG.maxPosts && scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = posts.size;
document.querySelectorAll($tweet).forEach(tweet => {
const timeLink = tweet.querySelector('a[href*="/status/"] time')?.closest('a');
const tweetUrl = timeLink?.getAttribute('href') || '';
const tweetId = tweetUrl.split('/status/')[1]?.split('?')[0];
if (!tweetId || posts.has(tweetId)) return;
const textEl = tweet.querySelector($tweetText);
const text = textEl?.textContent || '';
const timeEl = tweet.querySelector('time');
const timestamp = timeEl?.getAttribute('datetime');
const likeBtn = tweet.querySelector($like);
const retweetBtn = tweet.querySelector($retweet);
const replyBtn = tweet.querySelector($reply);
const likes = parseCount(likeBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const retweets = parseCount(retweetBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const replies = parseCount(replyBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
// Check for media
const hasImage = tweet.querySelector('[data-testid="tweetPhoto"]') !== null;
const hasVideo = tweet.querySelector('[data-testid="videoPlayer"], [data-testid="videoComponent"]') !== null;
const hasLink = tweet.querySelector('a[href*="t.co"]') !== null;
// Extract hashtags
const hashtags = (text.match(/#\w+/g) || []).map(h => h.toLowerCase());
posts.set(tweetId, {
id: tweetId,
text,
timestamp,
likes,
retweets,
replies,
engagement: likes + retweets + replies,
hasImage,
hasVideo,
hasLink,
hashtags,
url: `https://x.com${tweetUrl}`
});
});
if (posts.size === prevSize) retries++;
else retries = 0;
console.log(` Found ${posts.size} posts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
const postsArray = Array.from(posts.values());
if (postsArray.length === 0) {
console.error('❌ No posts found.');
return;
}
console.log(`\n✅ Analyzed ${postsArray.length} posts\n`);
const formatNum = (n) => {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return Math.round(n).toString();
};
// Calculate stats
const totals = {
likes: postsArray.reduce((sum, p) => sum + p.likes, 0),
retweets: postsArray.reduce((sum, p) => sum + p.retweets, 0),
replies: postsArray.reduce((sum, p) => sum + p.replies, 0)
};
const averages = {
likes: totals.likes / postsArray.length,
retweets: totals.retweets / postsArray.length,
replies: totals.replies / postsArray.length,
engagement: (totals.likes + totals.retweets + totals.replies) / postsArray.length
};
// Posting frequency
const postsWithTime = postsArray.filter(p => p.timestamp);
let postsPerDay = 0;
if (postsWithTime.length >= 2) {
const oldest = new Date(postsWithTime[postsWithTime.length - 1].timestamp);
const newest = new Date(postsWithTime[0].timestamp);
const days = Math.max(1, (newest - oldest) / (1000 * 60 * 60 * 24));
postsPerDay = postsWithTime.length / days;
}
// Content type analysis
const withImage = postsArray.filter(p => p.hasImage).length;
const withVideo = postsArray.filter(p => p.hasVideo).length;
const withLink = postsArray.filter(p => p.hasLink).length;
const textOnly = postsArray.filter(p => !p.hasImage && !p.hasVideo && !p.hasLink).length;
// Hashtag analysis
const hashtagCount = {};
postsArray.forEach(p => {
p.hashtags.forEach(h => {
hashtagCount[h] = (hashtagCount[h] || 0) + 1;
});
});
const topHashtags = Object.entries(hashtagCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
// Output
console.log('═'.repeat(60));
console.log(`📊 COMPETITOR ANALYSIS: @${username}`);
console.log('═'.repeat(60));
console.log('\n📈 ENGAGEMENT METRICS:');
console.log('─'.repeat(50));
console.log(` Avg ❤️ Likes per post: ${averages.likes.toFixed(1)}`);
console.log(` Avg 🔄 Retweets per post: ${averages.retweets.toFixed(1)}`);
console.log(` Avg 💬 Replies per post: ${averages.replies.toFixed(1)}`);
console.log(` Avg Total Engagement: ${averages.engagement.toFixed(1)}`);
if (profileStats.followers > 0) {
const engagementRate = (averages.engagement / profileStats.followers) * 100;
console.log(`\n 📐 Engagement Rate: ${engagementRate.toFixed(3)}%`);
console.log(` (avg engagement / followers)`);
}
console.log('\n📅 POSTING FREQUENCY:');
console.log('─'.repeat(50));
console.log(` Posts per day: ${postsPerDay.toFixed(1)}`);
console.log(` Posts per week: ${(postsPerDay * 7).toFixed(1)}`);
console.log('\n📷 CONTENT MIX:');
console.log('─'.repeat(50));
const total = postsArray.length;
console.log(` 📝 Text only: ${textOnly} (${((textOnly/total)*100).toFixed(0)}%)`);
console.log(` 🖼️ With image: ${withImage} (${((withImage/total)*100).toFixed(0)}%)`);
console.log(` 🎥 With video: ${withVideo} (${((withVideo/total)*100).toFixed(0)}%)`);
console.log(` 🔗 With link: ${withLink} (${((withLink/total)*100).toFixed(0)}%)`);
// Engagement by content type
console.log('\n📊 ENGAGEMENT BY CONTENT TYPE:');
console.log('─'.repeat(50));
const typeStats = [
{ type: '📝 Text only', posts: postsArray.filter(p => !p.hasImage && !p.hasVideo) },
{ type: '🖼️ With image', posts: postsArray.filter(p => p.hasImage) },
{ type: '🎥 With video', posts: postsArray.filter(p => p.hasVideo) }
];
typeStats.forEach(t => {
if (t.posts.length > 0) {
const avgEng = t.posts.reduce((sum, p) => sum + p.engagement, 0) / t.posts.length;
console.log(` ${t.type}: ${formatNum(avgEng)} avg (${t.posts.length} posts)`);
}
});
if (topHashtags.length > 0) {
console.log('\n#️⃣ TOP HASHTAGS:');
console.log('─'.repeat(50));
topHashtags.forEach(([tag, count], i) => {
console.log(` ${i + 1}. ${tag} (${count} times)`);
});
}
console.log('\n🏆 TOP PERFORMING POSTS:');
console.log('─'.repeat(50));
const topPosts = [...postsArray].sort((a, b) => b.engagement - a.engagement).slice(0, 5);
topPosts.forEach((p, i) => {
console.log(`\n ${i + 1}. ${formatNum(p.engagement)} total engagement`);
console.log(` ❤️ ${formatNum(p.likes)} | 🔄 ${formatNum(p.retweets)} | 💬 ${formatNum(p.replies)}`);
console.log(` "${p.text.slice(0, 80)}${p.text.length > 80 ? '...' : ''}"`);
console.log(` ${p.url}`);
});
// Save analysis
const storageKey = `xactions_competitor_${username}`;
const data = {
username,
timestamp: new Date().toISOString(),
profileStats,
postCount: postsArray.length,
averages,
postsPerDay,
contentMix: { textOnly, withImage, withVideo, withLink },
topHashtags,
topPosts: topPosts.map(p => ({ text: p.text.slice(0, 200), engagement: p.engagement, url: p.url }))
};
localStorage.setItem(storageKey, JSON.stringify(data));
console.log('\n' + '═'.repeat(60));
console.log('💡 INSIGHTS TO APPLY:');
console.log('═'.repeat(60));
const bestType = typeStats.sort((a, b) => {
const avgA = a.posts.length ? a.posts.reduce((s, p) => s + p.engagement, 0) / a.posts.length : 0;
const avgB = b.posts.length ? b.posts.reduce((s, p) => s + p.engagement, 0) / b.posts.length : 0;
return avgB - avgA;
})[0];
console.log(`\n1. Best content type: ${bestType.type}`);
console.log(`2. Posting frequency: ${postsPerDay.toFixed(1)} posts/day`);
if (topHashtags.length > 0) {
console.log(`3. Key hashtags: ${topHashtags.slice(0, 5).map(h => h[0]).join(', ')}`);
}
console.log('\n' + '═'.repeat(60));
console.log(`💾 Analysis saved! Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('═'.repeat(60) + '\n');
})();
});
register("continuous-monitor", function(){
var CONFIG = {
// How often to check (minutes)
checkIntervalMinutes: 5,
// Enable browser notifications
enableNotifications: true,
// Enable sound alert
enableSound: true,
// Scroll delay when scraping
scrollDelay: 1500,
// Max scrolls per check
maxScrolls: 50,
// Max retries when no new users
maxRetries: 3
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function continuousMonitor() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Re-pasting the script must not stack a second timer on top of a still-running
// one - that would double (then triple, ...) every check and every notification.
if (typeof window.stopMonitor === 'function') {
try { window.stopMonitor(); } catch (e) { /* ignore */ }
}
const $userCell = '[data-testid="UserCell"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔄 CONTINUOUS MONITOR ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Detect page
const url = window.location.href;
const pathMatch = url.match(/x\.com\/([^\/]+)\/(followers|following)/);
if (!pathMatch) {
console.error('❌ ERROR: Must be on a followers or following page!');
return;
}
const username = pathMatch[1];
const pageType = pathMatch[2];
const storageKey = `xactions_continuous_${username}_${pageType}`;
console.log(`👤 Monitoring: @${username}/${pageType}`);
console.log(`⏱️ Check interval: ${CONFIG.checkIntervalMinutes} minutes`);
console.log('');
// Request notification permission
if (CONFIG.enableNotifications && 'Notification' in window) {
if (Notification.permission === 'default') {
console.log('🔔 Requesting notification permission...');
await Notification.requestPermission();
}
if (Notification.permission === 'granted') {
console.log('✅ Notifications enabled');
} else {
console.log('⚠️ Notifications denied - will only show in console');
}
}
/**
* Play notification sound
*/
function playSound() {
if (!CONFIG.enableSound) return;
try {
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2teleVUYKm2i4LuUZz8WHVmIpbGniFo5S3+FdWVxdZGdnIVcNjtjlbzOtINGGSl5q9rSmFkkBEKY2OmpaC0UQoHPtHtGFRxii8DDoJxZKS9VhKSahpBdM0FYanRoZmNnc3F3d3V1c3ZzdnFyb2xnaGlubnFwbm1xdXh9g4SFgoKAf4B/e3Z1dHR0dHRyc3N0dHV2d3l7fHx8fH1+f4CBgoKCgYGBgYGAf39+fX18fHt7enp5eXl5eXl5');
audio.play().catch(() => {});
} catch (e) {}
}
/**
* Send notification
*/
function notify(title, body) {
if (CONFIG.enableNotifications && Notification.permission === 'granted') {
new Notification(title, { body, icon: 'https://abs.twimg.com/favicons/twitter.3.ico' });
}
playSound();
}
/**
* Get username from cell
*/
function getUsername(cell) {
const link = cell.querySelector('a[href^="/"]');
return link ? link.getAttribute('href')?.replace('/', '').split('/')[0] : null;
}
/**
* Scrape current users
*/
async function scrapeUsers() {
const users = new Set();
let lastCount = 0;
let retries = 0;
let scrolls = 0;
// Scroll to top first
window.scrollTo(0, 0);
await sleep(500);
while (scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
document.querySelectorAll($userCell).forEach(cell => {
const u = getUsername(cell);
if (u) users.add(u);
});
if (users.size === lastCount) {
retries++;
} else {
retries = 0;
lastCount = users.size;
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
// Scroll back to top
window.scrollTo(0, 0);
return users;
}
/**
* Perform a check
*/
let checkRunning = false;
async function performCheck() {
// A scrape can take longer than a short check interval; never let two
// checks scroll the page at the same time
if (checkRunning) return;
checkRunning = true;
try {
await runCheck();
} finally {
checkRunning = false;
}
}
async function runCheck() {
const checkTime = new Date().toLocaleTimeString();
console.log(`\n🔄 [${checkTime}] Checking for changes...`);
const currentUsers = await scrapeUsers();
console.log(` Scraped ${currentUsers.size} users`);
// Load previous
let previous = null;
try {
const saved = localStorage.getItem(storageKey);
if (saved) previous = JSON.parse(saved);
} catch (e) {}
// Save current
const snapshot = {
savedAt: new Date().toISOString(),
users: Array.from(currentUsers)
};
localStorage.setItem(storageKey, JSON.stringify(snapshot));
if (!previous) {
console.log(' 📸 First snapshot saved');
return;
}
// Compare
const prevSet = new Set(previous.users);
const removed = [];
prevSet.forEach(u => {
if (!currentUsers.has(u)) removed.push(u);
});
const added = [];
currentUsers.forEach(u => {
if (!prevSet.has(u)) added.push(u);
});
if (removed.length === 0 && added.length === 0) {
console.log(' ✅ No changes');
return;
}
// Changes detected!
console.log('');
console.log(' ⚡ CHANGES DETECTED!');
if (removed.length > 0) {
console.log(` 🚫 Removed (${removed.length}): ${removed.join(', ')}`);
notify(`🚫 ${removed.length} Unfollowers`, removed.slice(0, 3).map(u => '@' + u).join(', '));
}
if (added.length > 0) {
console.log(` 🆕 Added (${added.length}): ${added.join(', ')}`);
notify(`🆕 ${added.length} New Followers`, added.slice(0, 3).map(u => '@' + u).join(', '));
}
}
// Initial check
console.log('🚀 Running initial check...');
await performCheck();
// Schedule periodic checks
const intervalMs = CONFIG.checkIntervalMinutes * 60 * 1000;
const intervalId = setInterval(performCheck, intervalMs);
// Store stop function
window.stopMonitor = () => {
clearInterval(intervalId);
console.log('');
console.log('🛑 Monitoring stopped');
console.log('');
};
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ MONITORING ACTIVE ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`⏱️ Next check in ${CONFIG.checkIntervalMinutes} minutes`);
console.log('');
console.log('💡 Keep this tab open!');
console.log('💡 To stop: window.stopMonitor()');
console.log('');
})();
});
register("create-poll", function(){
(async function createPoll() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// The poll question / tweet text.
text: 'What should we build next?',
// 2 to 4 choices, max 25 characters each.
choices: ['Solana tools', 'Base tools', 'More avatars'],
// Poll duration. X caps a poll at 7 days total. Minimum is 5 minutes.
durationDays: 1,
durationHours: 0,
durationMinutes: 0,
// Preview only: validate and log without clicking Post.
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
composer: '[data-testid="tweetTextarea_0"]',
newTweetButton: 'a[data-testid="SideNav_NewTweet_Button"]',
addPoll: '[aria-label="Add poll"]',
addPollFallback: '[data-testid="pollButton"], [data-testid="createPoll"]',
choiceInput: '[data-testid="choiceInput"]',
// Older poll option inputs, kept as a fallback.
legacyChoice: (i) => `[data-testid="pollOption_${i}"], [data-testid="pollOptionTextInput_${i}"]`,
addChoice: '[data-testid="addPollOptionButton"], [aria-label="Add a choice"], [data-testid="addPollOption"]',
postButton: '[data-testid="tweetButton"]',
postButtonInline: '[data-testid="tweetButtonInline"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const waitForSelector = async (selector, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(200);
}
return null;
};
// Type into a contenteditable composer via a real input dispatch.
const typeIntoComposer = async (el, text) => {
el.focus();
await sleep(150);
const inserted = document.execCommand('insertText', false, text);
if (!inserted) {
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));
el.textContent = text;
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: text, bubbles: true }));
}
await sleep(250);
};
// Poll choice fields are native <input>. Use the native value setter so React
// picks up the change, then dispatch input. Falls back to execCommand for a
// contenteditable choice field.
const typeIntoField = async (el, text) => {
el.focus();
await sleep(120);
const tag = el.tagName.toLowerCase();
if (tag === 'input' || tag === 'textarea') {
const proto = tag === 'input' ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(el, text);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
const inserted = document.execCommand('insertText', false, text);
if (!inserted) {
el.textContent = text;
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: text, bubbles: true }));
}
}
await sleep(250);
};
const setSelectByText = (select, wanted) => {
if (!select) return false;
const target = String(wanted).trim().toLowerCase();
const opts = Array.from(select.options);
const match =
opts.find((o) => o.textContent.trim().toLowerCase() === target) ||
opts.find((o) => o.value.trim().toLowerCase() === target) ||
opts.find((o) => o.textContent.trim().toLowerCase().startsWith(target));
if (!match) return false;
select.value = match.value;
select.dispatchEvent(new Event('input', { bubbles: true }));
select.dispatchEvent(new Event('change', { bubbles: true }));
return true;
};
const findSelectByLabel = (root, label) => {
const selects = Array.from(root.querySelectorAll('select'));
const l = label.toLowerCase();
return selects.find((s) => (s.getAttribute('aria-label') || '').toLowerCase().includes(l)) || null;
};
const getChoiceInputs = () => {
let inputs = Array.from(document.querySelectorAll(SELECTORS.choiceInput));
if (inputs.length === 0) {
for (let i = 0; i < 4; i++) {
const el = document.querySelector(SELECTORS.legacyChoice(i));
if (el) inputs.push(el);
}
}
return inputs;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { typed: false, choicesFilled: 0, durationSet: false, posted: false, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 CREATE POLL - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
try {
const host = window.location.hostname;
if (!/(^|\.)x\.com$/.test(host) && !/(^|\.)twitter\.com$/.test(host)) {
log.warning('Not on x.com. Open x.com/compose/post, then re-run.');
return stats;
}
const choices = (CONFIG.choices || []).map((c) => (c || '').trim()).filter(Boolean);
if (choices.length < 2 || choices.length > 4) {
log.error(`Polls need 2 to 4 choices. You provided ${choices.length}.`);
return stats;
}
const tooLong = choices.filter((c) => c.length > 25);
if (tooLong.length > 0) {
log.error(`Each choice must be <= 25 characters. Too long: ${tooLong.join(', ')}`);
return stats;
}
if (!CONFIG.text || !CONFIG.text.trim()) {
log.error('CONFIG.text is empty. Set the poll question.');
return stats;
}
const totalMinutes = (CONFIG.durationDays * 24 * 60) + (CONFIG.durationHours * 60) + CONFIG.durationMinutes;
if (totalMinutes < 5) {
log.error('Poll duration must be at least 5 minutes.');
return stats;
}
if (totalMinutes > 7 * 24 * 60) {
log.error('Poll duration cannot exceed 7 days.');
return stats;
}
log.info(`Question: "${CONFIG.text}"`);
choices.forEach((c, i) => log.info(`Choice ${i + 1}: "${c}" (${c.length}/25)`));
log.info(`Duration: ${CONFIG.durationDays}d ${CONFIG.durationHours}h ${CONFIG.durationMinutes}m`);
if (CONFIG.dryRun) {
log.warning('DRY RUN. Nothing posted. Set CONFIG.dryRun = false to post for real.');
return stats;
}
// Open the composer.
let composer = document.querySelector(SELECTORS.composer);
if (!composer) {
const newBtn = document.querySelector(SELECTORS.newTweetButton);
if (newBtn) {
newBtn.click();
composer = await waitForSelector(SELECTORS.composer, 6000);
}
}
if (!composer) {
log.error('Composer not found. Open x.com/compose/post and re-run.');
stats.errors++;
return stats;
}
await typeIntoComposer(composer, CONFIG.text);
stats.typed = true;
log.success('Question typed.');
// Open the poll UI.
const pollBtn =
document.querySelector(SELECTORS.addPoll) ||
document.querySelector(SELECTORS.addPollFallback);
if (!pollBtn) {
log.error('Poll button not found in the composer toolbar.');
stats.errors++;
return stats;
}
pollBtn.click();
await sleep(1200);
// Fill choices, adding extra slots for choice 3 and 4.
for (let i = 0; i < choices.length; i++) {
if (i >= 2) {
const addBtn = document.querySelector(SELECTORS.addChoice);
if (addBtn) {
addBtn.click();
await sleep(600);
}
}
const inputs = getChoiceInputs();
const field = inputs[i];
if (!field) {
log.warning(`Choice ${i + 1} input not found. Fill it by hand.`);
continue;
}
await typeIntoField(field, choices[i]);
stats.choicesFilled++;
log.success(`Choice ${i + 1}: "${choices[i]}"`);
}
// Set duration via the Days / Hours / Minutes selects if present.
const daysSel = findSelectByLabel(document, 'days');
const hoursSel = findSelectByLabel(document, 'hours');
const minsSel = findSelectByLabel(document, 'minutes');
if (daysSel || hoursSel || minsSel) {
const dOk = daysSel ? setSelectByText(daysSel, String(CONFIG.durationDays)) : true;
const hOk = hoursSel ? setSelectByText(hoursSel, String(CONFIG.durationHours)) : true;
const mOk = minsSel ? setSelectByText(minsSel, String(CONFIG.durationMinutes)) : true;
stats.durationSet = dOk && hOk && mOk;
if (stats.durationSet) log.success('Poll duration set.');
else log.warning('Some duration fields could not be set. Verify them by hand.');
await sleep(400);
} else {
log.warning('Duration selects not found. Default (1 day) will be used unless you change it.');
}
await sleep(500);
const postBtn =
document.querySelector(SELECTORS.postButton) ||
document.querySelector(SELECTORS.postButtonInline);
if (!postBtn) {
log.error('Post button not found. Poll is filled. Click Post manually.');
stats.errors++;
return stats;
}
if (postBtn.getAttribute('aria-disabled') === 'true' || postBtn.disabled) {
log.error('Post button is disabled. Check that every choice is filled.');
stats.errors++;
return stats;
}
postBtn.click();
await sleep(2500);
const stillOpen = document.querySelector(SELECTORS.composer);
const openText = stillOpen ? (stillOpen.textContent || '').trim() : '';
if (!stillOpen || openText.length === 0) {
stats.posted = true;
log.success('Poll posted.');
} else {
log.warning('Composer still open. The poll may not have posted. Check the page.');
stats.errors++;
}
} catch (error) {
log.error(`Error: ${error.message}`);
stats.errors++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 CREATE POLL - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ⌨️ Question typed: ${String(stats.typed).padEnd(32)}║
║ 📋 Choices filled: ${String(stats.choicesFilled).padEnd(32)}║
║ ⏱️ Duration set: ${String(stats.durationSet).padEnd(32)}║
║ ✅ Posted: ${String(stats.posted).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("delete-tweets", function(){
(async function deleteTweets() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// 🔒 SAFETY: true = preview only (logs what WOULD be deleted, deletes nothing).
// false = posts are PERMANENTLY deleted. Set this to true for a preview run
// whenever you change the filters below.
dryRun: false,
// Only delete posts older than this many days. 0 = any age.
olderThanDays: 0,
// Only delete posts whose text contains AT LEAST ONE of these
// (case-insensitive). Empty array = ignore keywords (matches all).
containingKeywords: [],
// Protect high performers: skip any post with at least this many likes.
// Set to a high number to keep your best posts. 0 = delete regardless.
minLikesToKeep: 0,
// Maximum number of posts to delete this run
maxDeletes: 25,
// Minimum delay between deletions (ms). X throttles bursts of deletes and
// starts failing them silently, so these are deliberately unhurried.
minDelay: 3000,
// Maximum delay between deletions (ms)
maxDelay: 7000,
// Delay after each scroll, to let the next page of posts render (ms)
scrollDelay: 4000,
// Wait after opening a post's menu / clicking through it (ms)
menuDelay: 1200,
// Seconds to wait before the first real deletion, so a mistake is
// stoppable. 0 = start immediately.
countdownSeconds: 5,
// Maximum scroll attempts to load more posts
maxScrollAttempts: 60,
// Stop if no new posts appear after this many consecutive scrolls
noNewPostsThreshold: 5
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
caret: '[data-testid="caret"]',
likeButton: '[data-testid="like"], [data-testid="unlike"]',
socialContext: '[data-testid="socialContext"]',
confirm: '[data-testid="confirmationSheetConfirm"]',
menuItem: '[role="menuitem"]',
profileLink: 'a[data-testid="AppTabBar_Profile_Link"]',
accountSwitcher: '[data-testid="SideNav_AccountSwitcher_Button"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => window.scrollTo(0, document.body.scrollHeight);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} processed`)
};
const parseCount = (str) => {
if (!str) return 0;
const m = str.replace(/,/g, '').match(/([\d.]+)\s*([KMB])?/i);
if (!m) return 0;
let n = parseFloat(m[1]);
if (m[2]) n *= { K: 1e3, M: 1e6, B: 1e9 }[m[2].toUpperCase()] || 1;
return Math.round(n);
};
const download = (data, filename) => {
try {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
log.info(`Saved a log of this run: ${filename}`);
} catch (e) {
log.warning(`Could not save the run log: ${e.message}`);
}
};
const getTweetId = (tweet) => {
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const link = tweet.querySelector('a[href*="/status/"]');
const m = link?.getAttribute('href')?.match(/\/status\/(\d+)/);
return m ? m[1] : null;
};
const getLikes = (tweet) => {
const el = tweet.querySelector(SELECTORS.likeButton);
return parseCount(el?.getAttribute('aria-label') || '0');
};
const getTimestamp = (tweet) => tweet.querySelector('time')?.getAttribute('datetime') || '';
const isPinned = (tweet) => /pinned/i.test(tweet.querySelector(SELECTORS.socialContext)?.textContent || '');
const isRepost = (tweet) => /reposted/i.test(tweet.querySelector(SELECTORS.socialContext)?.textContent || '');
// The signed-in handle, read from the left nav: the profile link on wide
// layouts, the account switcher on narrow ones. Null when neither rendered.
const getOwnHandle = () => {
const href = document.querySelector(SELECTORS.profileLink)?.getAttribute('href') || '';
const fromNav = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
if (fromNav) return fromNav[1];
const switcher = document.querySelector(SELECTORS.accountSwitcher)?.textContent || '';
const fromSwitcher = switcher.match(/@([A-Za-z0-9_]{1,15})/);
return fromSwitcher ? fromSwitcher[1] : null;
};
// The timeline hydrates after the shell paints, so poll instead of giving up
// on the first empty query.
const waitForPosts = async (timeoutMs = 15000) => {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (document.querySelector(SELECTORS.tweet)) return true;
await sleep(500);
}
return false;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
deleted: 0,
wouldDelete: 0,
skippedTooNew: 0,
skippedNoKeyword: 0,
skippedHighPerformer: 0,
skippedNotYours: 0,
skippedPinned: 0,
errors: 0
};
const processed = new Set();
const deleteLog = [];
// Stop switch: run window.stopDeleteTweets() to abort after the current post.
let stopped = false;
window.stopDeleteTweets = () => {
stopped = true;
log.warning('Stop requested. Finishing the current post, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🗑️ DELETE TWEETS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: this has to run on your own profile timeline. X serves that at
// /<handle> and its tabs at /<handle>/<tab>, so accept both; anything else
// (a feed, a single status, a reserved section) is not a profile. Warn, do
// not redirect.
const PROFILE_TABS = ['with_replies', 'media', 'likes', 'highlights', 'articles', 'superfollows', 'affiliates'];
const RESERVED = ['home', 'explore', 'notifications', 'messages', 'i', 'search', 'settings', 'compose',
'hashtag', 'bookmarks', 'lists', 'topics', 'communities', 'jobs', 'tos', 'privacy'];
const pathMatch = window.location.pathname.match(/^\/([A-Za-z0-9_]{1,15})(?:\/([A-Za-z0-9_]+))?\/?$/);
const pathUser = pathMatch && !RESERVED.includes(pathMatch[1].toLowerCase()) ? pathMatch[1] : null;
const onProfile = Boolean(pathUser) && (!pathMatch[2] || PROFILE_TABS.includes(pathMatch[2].toLowerCase()));
const ownHandle = getOwnHandle();
if (!onProfile) {
log.warning(ownHandle
? `Run this on your profile timeline. Open https://x.com/${ownHandle} and run it again.`
: 'Run this on your profile timeline (x.com/YOUR_USERNAME), then run it again.');
return;
}
if (ownHandle && pathUser.toLowerCase() !== ownHandle.toLowerCase()) {
log.error(`This is @${pathUser}'s profile. You can only delete your own posts: open https://x.com/${ownHandle} and run it again.`);
return;
}
const profileUser = ownHandle || pathUser;
log.info(`Profile: @${profileUser}`);
if (!ownHandle) {
log.warning('Could not confirm the signed-in account from the page. Make sure this is YOUR profile before letting the countdown finish.');
}
// Loud destructive warning.
if (CONFIG.dryRun) {
console.log('%c🔍 DRY RUN - preview only. NOTHING will be deleted.', 'color:#1d9bf0;font-weight:bold;font-size:14px');
console.log('%c To actually delete, set CONFIG.dryRun = false and run again.', 'color:#1d9bf0');
} else {
console.log('%c⚠️⚠️⚠️ LIVE MODE - posts WILL be PERMANENTLY DELETED. This CANNOT be undone. ⚠️⚠️⚠️', 'color:#f4212e;font-weight:bold;font-size:15px');
for (let i = CONFIG.countdownSeconds; i > 0 && !stopped; i--) {
log.warning(`Starting in ${i}s. Run window.stopDeleteTweets() now to abort.`);
await sleep(1000);
}
if (stopped) {
log.warning('Aborted before deleting anything.');
return stats;
}
}
const cutoff = CONFIG.olderThanDays > 0 ? new Date(Date.now() - CONFIG.olderThanDays * 86400000) : null;
log.info(`Max deletes: ${CONFIG.maxDeletes} | Older than: ${CONFIG.olderThanDays || 'any'} days | Keep >= ${CONFIG.minLikesToKeep} likes`);
log.info(`Keywords: ${CONFIG.containingKeywords.length ? CONFIG.containingKeywords.join(', ') : 'none (matches all)'}`);
log.info('To stop early: window.stopDeleteTweets()');
if (!document.querySelector(SELECTORS.tweet)) {
log.info('Waiting for your posts to render...');
}
if (!(await waitForPosts())) {
log.warning('No posts rendered after 15s. Reload the page, let the timeline appear, then run this again.');
return;
}
let scrollAttempts = 0;
let noNewPostsCount = 0;
while (!stopped && stats.deleted < CONFIG.maxDeletes && stats.wouldDelete < CONFIG.maxDeletes && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewPost = false;
for (const tweet of tweets) {
if (stopped) break;
if (CONFIG.dryRun ? stats.wouldDelete >= CONFIG.maxDeletes : stats.deleted >= CONFIG.maxDeletes) break;
// A live deletion re-renders the timeline, which detaches nodes still
// held by this snapshot; clicking one does nothing.
if (!tweet.isConnected) continue;
const tweetId = getTweetId(tweet);
if (!tweetId || processed.has(tweetId)) continue;
processed.add(tweetId);
foundNewPost = true;
try {
if (isPinned(tweet)) {
stats.skippedPinned++;
continue;
}
// Ownership: a repost by you counts; otherwise the author link must be you.
const repost = isRepost(tweet);
const ownAuthorLink = tweet.querySelector(`a[href="/${profileUser}" i]`);
if (!repost && !ownAuthorLink) {
stats.skippedNotYours++;
continue;
}
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
const timestamp = getTimestamp(tweet);
const likes = getLikes(tweet);
// Filter: age
if (cutoff && timestamp && new Date(timestamp) > cutoff) {
stats.skippedTooNew++;
continue;
}
// Filter: keywords
if (CONFIG.containingKeywords.length > 0) {
const lower = text.toLowerCase();
if (!CONFIG.containingKeywords.some(kw => lower.includes(kw.toLowerCase()))) {
stats.skippedNoKeyword++;
continue;
}
}
// Filter: protect high performers
if (CONFIG.minLikesToKeep > 0 && likes >= CONFIG.minLikesToKeep) {
stats.skippedHighPerformer++;
continue;
}
const preview = text.slice(0, 60).replace(/\n/g, ' ') || (repost ? '(repost)' : '(no text)');
if (CONFIG.dryRun) {
stats.wouldDelete++;
deleteLog.push({ tweetId, text: text.slice(0, 200), likes, timestamp, repost, dryRun: true });
console.log(`🔍 [DRY] Would delete #${stats.wouldDelete}: "${preview}..." (❤️ ${likes})`);
continue;
}
// LIVE deletion via the post's overflow (caret) menu.
const caret = tweet.querySelector(SELECTORS.caret);
if (!caret) continue;
caret.scrollIntoView({ block: 'center' });
await sleep(400);
caret.click();
await sleep(CONFIG.menuDelay);
const menuItems = document.querySelectorAll(SELECTORS.menuItem);
let actionBtn = null;
for (const item of menuItems) {
if (/delete/i.test(item.textContent) || /undo repost/i.test(item.textContent)) {
actionBtn = item;
break;
}
}
if (!actionBtn) {
document.body.click();
await sleep(600);
continue;
}
const wasRepost = /undo repost/i.test(actionBtn.textContent);
actionBtn.click();
await sleep(CONFIG.menuDelay);
// Deleting an original post asks for confirmation; undo-repost is instant.
if (!wasRepost) {
const confirm = document.querySelector(SELECTORS.confirm);
if (!confirm) {
document.body.click();
await sleep(600);
continue;
}
confirm.click();
}
stats.deleted++;
deleteLog.push({ tweetId, text: text.slice(0, 200), likes, timestamp, repost: wasRepost });
log.success(`Deleted #${stats.deleted}: "${preview}..." (❤️ ${likes})`);
log.progress(stats.deleted, CONFIG.maxDeletes);
await randomDelay();
} catch (error) {
log.error(`Error processing post: ${error.message}`);
stats.errors++;
document.body.click();
await sleep(1500);
}
}
if (!foundNewPost) {
noNewPostsCount++;
if (noNewPostsCount >= CONFIG.noNewPostsThreshold) {
log.warning('No new posts found after multiple scrolls. Stopping.');
break;
}
} else {
noNewPostsCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Scrolling for more posts... (attempt ${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(CONFIG.scrollDelay);
}
// ============================================
// 📊 SUMMARY
// ============================================
const totalSkipped = stats.skippedTooNew + stats.skippedNoKeyword + stats.skippedHighPerformer + stats.skippedNotYours + stats.skippedPinned;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 DELETE TWEETS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🗑️ Deleted: ${String(stats.deleted).padEnd(29)}║
║ 🔍 Would Delete (dry): ${String(stats.wouldDelete).padEnd(29)}║
║ ⏭️ Total Skipped: ${String(totalSkipped).padEnd(29)}║
║ └─ Too New: ${String(stats.skippedTooNew).padEnd(29)}║
║ └─ No Keyword: ${String(stats.skippedNoKeyword).padEnd(29)}║
║ └─ High Performer: ${String(stats.skippedHighPerformer).padEnd(29)}║
║ └─ Not Yours: ${String(stats.skippedNotYours).padEnd(29)}║
║ └─ Pinned: ${String(stats.skippedPinned).padEnd(29)}║
║ ❌ Errors: ${String(stats.errors).padEnd(29)}║
║ 📜 Scroll Attempts: ${String(scrollAttempts).padEnd(29)}║
╚══════════════════════════════════════════════════════════╝
`);
if (deleteLog.length > 0) {
download(
{ profile: profileUser, stats, dryRun: CONFIG.dryRun, tweets: deleteLog },
`xactions-delete-tweets-${new Date().toISOString().slice(0, 10)}.json`
);
}
if (CONFIG.dryRun && stats.wouldDelete > 0) {
console.log(`%c⚡ Reviewed ${stats.wouldDelete} post(s). Set CONFIG.dryRun = false and re-run to PERMANENTLY delete them.`, 'color:#f4212e;font-weight:bold');
} else if (!CONFIG.dryRun && stats.deleted >= CONFIG.maxDeletes) {
log.info('Hit maxDeletes for this run. Re-run the script to continue where it stopped.');
} else if (CONFIG.dryRun) {
log.info('Nothing matched your filters. Loosen olderThanDays / keywords / minLikesToKeep.');
}
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("detect-unfollowers", function(){
var CONFIG = {
// Delay between scrolls
scrollDelay: 2000,
// Maximum scroll attempts
maxScrolls: 100,
// Retry when no new users found
maxRetries: 5,
// Auto-download report
autoDownload: true,
// Storage key for snapshot
storageKey: 'xactions_my_followers'
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function detectUnfollowers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $userCell = '[data-testid="UserCell"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔍 DETECT UNFOLLOWERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify page
if (!window.location.href.includes('/followers')) {
console.error('❌ ERROR: You must be on your Followers page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME/followers');
return;
}
console.log('🚀 Scraping current followers...');
console.log('📜 Auto-scrolling to load all followers...');
console.log('');
/**
* Extract username from user cell
*/
function getUsername(cell) {
const link = cell.querySelector('a[href^="/"]');
if (link) {
const href = link.getAttribute('href');
return href ? href.replace('/', '').split('/')[0] : null;
}
return null;
}
/**
* Extract display name from user cell
*/
function getDisplayName(cell) {
const span = cell.querySelector('[dir="ltr"] span');
return span ? span.textContent : null;
}
// Scrape all followers
const currentFollowers = new Map(); // username -> displayName
let lastCount = 0;
let retries = 0;
let scrolls = 0;
while (scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const cells = document.querySelectorAll($userCell);
cells.forEach(cell => {
const username = getUsername(cell);
const displayName = getDisplayName(cell);
if (username && !currentFollowers.has(username)) {
currentFollowers.set(username, displayName || username);
}
});
if (currentFollowers.size === lastCount) {
retries++;
} else {
retries = 0;
lastCount = currentFollowers.size;
}
console.log(`📊 Scraped ${currentFollowers.size} followers...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log('');
console.log(`✅ Finished scraping: ${currentFollowers.size} followers`);
// Load previous snapshot
let previousSnapshot = null;
try {
const saved = localStorage.getItem(CONFIG.storageKey);
if (saved) {
previousSnapshot = JSON.parse(saved);
}
} catch (e) {
console.warn('⚠️ Could not load previous snapshot');
}
const timestamp = new Date().toISOString();
// Save current snapshot
const snapshot = {
savedAt: timestamp,
count: currentFollowers.size,
followers: Object.fromEntries(currentFollowers)
};
localStorage.setItem(CONFIG.storageKey, JSON.stringify(snapshot));
console.log('💾 Saved new snapshot to localStorage');
console.log('');
// Compare if we have previous data
if (!previousSnapshot) {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📸 FIRST SNAPSHOT SAVED! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 Followers saved: ${currentFollowers.size}`);
console.log('');
console.log('💡 Run this script again later to detect unfollowers!');
window.followerSnapshot = snapshot;
return snapshot;
}
// Compare snapshots
console.log('🔄 Comparing with previous snapshot...');
console.log(` Previous: ${previousSnapshot.count} followers (${previousSnapshot.savedAt})`);
console.log(` Current: ${currentFollowers.size} followers`);
console.log('');
const previousUsernames = new Set(Object.keys(previousSnapshot.followers));
const currentUsernames = new Set(currentFollowers.keys());
// Find unfollowers (in previous but not current)
const unfollowers = [];
previousUsernames.forEach(username => {
if (!currentUsernames.has(username)) {
unfollowers.push({
username,
displayName: previousSnapshot.followers[username]
});
}
});
// Find new followers (in current but not previous)
const newFollowers = [];
currentUsernames.forEach(username => {
if (!previousUsernames.has(username)) {
newFollowers.push({
username,
displayName: currentFollowers.get(username)
});
}
});
// Display results
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📊 RESULTS ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
if (unfollowers.length > 0) {
console.log(`🚫 UNFOLLOWERS (${unfollowers.length}):`);
unfollowers.forEach((u, i) => {
console.log(` ${i + 1}. @${u.username} (${u.displayName})`);
console.log(` https://x.com/${u.username}`);
});
console.log('');
} else {
console.log('✅ No unfollowers detected!');
console.log('');
}
if (newFollowers.length > 0) {
console.log(`🆕 NEW FOLLOWERS (${newFollowers.length}):`);
newFollowers.forEach((u, i) => {
console.log(` ${i + 1}. @${u.username} (${u.displayName})`);
});
console.log('');
} else {
console.log('📭 No new followers since last check.');
console.log('');
}
// Download report
if (CONFIG.autoDownload && (unfollowers.length > 0 || newFollowers.length > 0)) {
let report = `UNFOLLOWER DETECTION REPORT\n`;
report += `${'='.repeat(50)}\n`;
report += `Generated: ${timestamp}\n`;
report += `Previous snapshot: ${previousSnapshot.savedAt}\n`;
report += `Previous count: ${previousSnapshot.count}\n`;
report += `Current count: ${currentFollowers.size}\n`;
report += `${'='.repeat(50)}\n\n`;
if (unfollowers.length > 0) {
report += `UNFOLLOWERS (${unfollowers.length}):\n`;
report += `${'-'.repeat(30)}\n`;
unfollowers.forEach((u, i) => {
report += `${i + 1}. @${u.username} (${u.displayName})\n`;
report += ` https://x.com/${u.username}\n`;
});
report += '\n';
}
if (newFollowers.length > 0) {
report += `NEW FOLLOWERS (${newFollowers.length}):\n`;
report += `${'-'.repeat(30)}\n`;
newFollowers.forEach((u, i) => {
report += `${i + 1}. @${u.username} (${u.displayName})\n`;
});
}
const blob = new Blob([report], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `unfollowers_${timestamp.split('T')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('💾 Report downloaded!');
}
const result = {
timestamp,
previous: previousSnapshot.count,
current: currentFollowers.size,
unfollowers,
newFollowers
};
window.unfollowerReport = result;
console.log('');
console.log('💡 Access data via: window.unfollowerReport');
return result;
})();
});
register("edit-profile", function(){
(async function editProfile() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Display name (max 50 chars). null = leave unchanged.
name: null,
// Bio / description (max 160 chars). null = leave unchanged.
bio: null,
// Location string. null = leave unchanged. '' = clear the field.
location: null,
// Website URL. null = leave unchanged. '' = clear the field.
website: null,
// Birthdate as { month: 'March', day: 14, year: 1990 }. null = leave unchanged.
// month accepts a full month name ('March') or number (3). Any subset of
// month/day/year you provide will be set; omitted parts are left as-is.
birthdate: null,
// Click Save automatically after filling. If false, fields are filled and
// you click Save yourself.
autoSave: true,
// Delay for UI interactions (ms).
actionDelay: 700
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
editButton: '[data-testid="editProfileButton"]',
// X has shipped several testids/name attrs over time; try each in order.
nameInput: [
'[data-testid="displayNameInput"]',
'input[name="displayName"]',
'input[name="name"]'
],
bioInput: [
'[data-testid="bioTextarea"]',
'[data-testid="descriptionInput"]',
'[data-testid="ocf-bio-input"]',
'textarea[name="description"]'
],
locationInput: [
'[data-testid="locationInput"]',
'input[name="location"]'
],
websiteInput: [
'[data-testid="urlInput"]',
'input[name="url"]'
],
saveButton: '[data-testid="Profile_Save_Button"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
// Resolve the first matching element from an array of selectors (or a single string).
const findFirst = (selectors) => {
const list = Array.isArray(selectors) ? selectors : [selectors];
for (const sel of list) {
const el = document.querySelector(sel);
if (el) return el;
}
return null;
};
// Set an input/textarea value through the native setter so React's value
// tracker registers the change (a plain .value assignment is ignored by
// React and the old value would be saved).
const setNativeValue = (el, value) => {
const proto = el instanceof HTMLTextAreaElement
? HTMLTextAreaElement.prototype
: HTMLInputElement.prototype;
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
if (desc && desc.set) desc.set.call(el, value);
else el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
// Fill one text field. Returns 'set' | 'notfound'.
const fillField = async (label, selectors, value) => {
const el = findFirst(selectors);
if (!el) {
log.warning(`${label} field not found on the page. Skipped.`);
return 'notfound';
}
el.focus();
await sleep(150);
setNativeValue(el, '');
await sleep(120);
setNativeValue(el, value);
await sleep(CONFIG.actionDelay);
const preview = value.length > 60 ? value.slice(0, 60) + '…' : value;
log.success(`${label} → "${preview}"`);
return 'set';
};
// Set a native <select> to the option whose text or value matches `wanted`.
const setSelectByText = (selectEl, wanted) => {
const target = String(wanted).trim().toLowerCase();
for (const opt of selectEl.options) {
const txt = opt.textContent.trim().toLowerCase();
const val = String(opt.value).trim().toLowerCase();
if (txt === target || val === target) {
setNativeValue(selectEl, opt.value);
return true;
}
}
// Numeric fallback (e.g. month '3' matching option value/text '3').
for (const opt of selectEl.options) {
if (parseInt(opt.value, 10) === parseInt(wanted, 10) && !Number.isNaN(parseInt(wanted, 10))) {
setNativeValue(selectEl, opt.value);
return true;
}
}
return false;
};
// Locate the three birthdate <select>s by their option content.
const findBirthdateSelects = () => {
const selects = Array.from(document.querySelectorAll('select'));
const out = { month: null, day: null, year: null };
for (const sel of selects) {
const texts = Array.from(sel.options).map(o => o.textContent.trim().toLowerCase());
const joined = texts.join(' ');
if (!out.month && (joined.includes('january') || joined.includes('month'))) out.month = sel;
else if (!out.year && texts.some(t => /^(19|20)\d\d$/.test(t))) out.year = sel;
else if (!out.day && texts.some(t => /^\d{1,2}$/.test(t)) && sel.options.length <= 40) out.day = sel;
}
return out;
};
const setBirthdate = async (bd) => {
const sel = findBirthdateSelects();
let anyField = false;
if (bd.month != null && sel.month) {
if (setSelectByText(sel.month, bd.month)) { log.success(`Birth month → ${bd.month}`); anyField = true; }
else log.warning(`Birth month "${bd.month}" not found in the dropdown.`);
await sleep(CONFIG.actionDelay);
}
if (bd.day != null && sel.day) {
if (setSelectByText(sel.day, bd.day)) { log.success(`Birth day → ${bd.day}`); anyField = true; }
else log.warning(`Birth day "${bd.day}" not found in the dropdown.`);
await sleep(CONFIG.actionDelay);
}
if (bd.year != null && sel.year) {
if (setSelectByText(sel.year, bd.year)) { log.success(`Birth year → ${bd.year}`); anyField = true; }
else log.warning(`Birth year "${bd.year}" not found in the dropdown.`);
await sleep(CONFIG.actionDelay);
}
if (!sel.month && !sel.day && !sel.year) {
log.warning('Birthdate dropdowns not found. If your profile has no birthdate yet, add one manually once, then this tool can update it.');
return 'notfound';
}
return anyField ? 'set' : 'notfound';
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { set: 0, notFound: 0, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📝 EDIT PROFILE - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn, do not hard-redirect (a redirect would kill this script).
const onProfileSettings = /\/settings\/profile/.test(window.location.pathname);
const onOwnProfile = !!document.querySelector(SELECTORS.editButton);
if (!onProfileSettings && !onOwnProfile) {
log.warning('Go to x.com/settings/profile (or open your own profile so the "Edit profile" button is visible), then run this again.');
return stats;
}
// Validate lengths before touching anything.
if (CONFIG.name != null && CONFIG.name.length > 50) {
log.error(`Display name is ${CONFIG.name.length} chars (max 50). Shorten it and re-run.`);
return stats;
}
if (CONFIG.bio != null && CONFIG.bio.length > 160) {
log.error(`Bio is ${CONFIG.bio.length} chars (max 160). Shorten it and re-run.`);
return stats;
}
const requested = [];
if (CONFIG.name != null) requested.push('name');
if (CONFIG.bio != null) requested.push('bio');
if (CONFIG.location != null) requested.push('location');
if (CONFIG.website != null) requested.push('website');
if (CONFIG.birthdate != null) requested.push('birthdate');
if (requested.length === 0) {
log.warning('Nothing to change. Set at least one field (name, bio, location, website, birthdate) in CONFIG.');
return stats;
}
log.info(`Fields to update: ${requested.join(', ')}`);
// Open the Edit profile dialog if the form is not already mounted.
const formPresent = () => findFirst(SELECTORS.nameInput) || findFirst(SELECTORS.bioInput);
if (!formPresent()) {
const editBtn = document.querySelector(SELECTORS.editButton);
if (editBtn) {
log.info('Opening the Edit profile dialog...');
editBtn.click();
}
// The dialog / settings form may still be mounting on first paint.
let waited = 0;
while (!formPresent() && waited < 6) {
await sleep(CONFIG.actionDelay);
waited++;
}
}
if (!formPresent()) {
log.error('Could not find the profile form. Make sure you are on x.com/settings/profile or that the Edit profile dialog opened.');
return stats;
}
const tally = (result) => {
if (result === 'set') stats.set++;
else if (result === 'notfound') stats.notFound++;
};
try {
if (CONFIG.name != null) tally(await fillField('Name', SELECTORS.nameInput, CONFIG.name));
if (CONFIG.bio != null) tally(await fillField('Bio', SELECTORS.bioInput, CONFIG.bio));
if (CONFIG.location != null) tally(await fillField('Location', SELECTORS.locationInput, CONFIG.location));
if (CONFIG.website != null) tally(await fillField('Website', SELECTORS.websiteInput, CONFIG.website));
if (CONFIG.birthdate != null) tally(await setBirthdate(CONFIG.birthdate));
} catch (error) {
log.error(`Error while filling fields: ${error.message}`);
stats.errors++;
}
if (stats.set === 0) {
log.warning('No fields were actually changed (none of the requested fields were found). Nothing to save.');
} else if (CONFIG.autoSave) {
const saveBtn = document.querySelector(SELECTORS.saveButton);
if (saveBtn) {
log.info('Saving profile...');
saveBtn.click();
await sleep(CONFIG.actionDelay * 2);
log.success('Save clicked.');
} else {
log.warning('Save button not found. Click "Save" in the dialog to apply your changes.');
}
} else {
log.info('autoSave is off. Click "Save" in the dialog to apply your changes.');
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 EDIT PROFILE - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Fields updated: ${String(stats.set).padEnd(32)}║
║ ⚠️ Fields not found: ${String(stats.notFound).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("engagement-analytics", function(){
var CONFIG = {
// Number of posts to analyze
maxPosts: 50,
// Delay between scrolls (ms)
scrollDelay: 1500,
// Maximum scroll attempts
maxScrolls: 30,
// Retry when no new posts found
maxRetries: 3
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function engagementAnalytics() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 📊 XActions — Engagement Analytics ║
║ Analyze likes, comments, retweets on your posts ║
╚══════════════════════════════════════════════════════════════╝
`);
// Selectors
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
const $like = '[data-testid="like"], [data-testid="unlike"]';
const $retweet = '[data-testid="retweet"], [data-testid="unretweet"]';
const $reply = '[data-testid="reply"]';
const $views = 'a[href*="/analytics"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '').trim();
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
const getUsername = () => {
const match = window.location.pathname.match(/^\/([^\/]+)/);
return match ? match[1] : null;
};
const username = getUsername();
if (!username || ['home', 'explore', 'notifications', 'messages', 'i'].includes(username)) {
console.error('❌ Please navigate to a profile page first!');
console.log('👉 Example: https://x.com/YOUR_USERNAME');
return;
}
console.log(`📊 Analyzing engagement for @${username}\n`);
console.log('🔄 Scrolling to load posts...\n');
const posts = new Map();
let retries = 0;
let scrollCount = 0;
while (posts.size < CONFIG.maxPosts && scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = posts.size;
document.querySelectorAll($tweet).forEach(tweet => {
// Check if this is from the profile owner
const userLinks = tweet.querySelectorAll('a[href^="/"]');
let isOwner = false;
userLinks.forEach(link => {
if (link.getAttribute('href')?.toLowerCase() === `/${username.toLowerCase()}`) {
isOwner = true;
}
});
if (!isOwner) return;
// Get tweet ID from time link
const timeLink = tweet.querySelector('a[href*="/status/"] time')?.closest('a');
const tweetUrl = timeLink?.getAttribute('href') || '';
const tweetId = tweetUrl.split('/status/')[1]?.split('?')[0];
if (!tweetId || posts.has(tweetId)) return;
// Get tweet text
const textEl = tweet.querySelector($tweetText);
const text = textEl?.textContent?.slice(0, 100) || '[No text]';
// Get engagement counts
const likeBtn = tweet.querySelector($like);
const retweetBtn = tweet.querySelector($retweet);
const replyBtn = tweet.querySelector($reply);
const viewsEl = tweet.querySelector($views);
const likes = parseCount(likeBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const retweets = parseCount(retweetBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const replies = parseCount(replyBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const views = parseCount(viewsEl?.textContent || '0');
// Get timestamp
const timeEl = tweet.querySelector('time');
const timestamp = timeEl?.getAttribute('datetime') || '';
posts.set(tweetId, {
id: tweetId,
text,
likes,
retweets,
replies,
views,
timestamp,
url: `https://x.com${tweetUrl}`
});
});
if (posts.size === prevSize) {
retries++;
} else {
retries = 0;
}
console.log(` Found ${posts.size} posts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
const postsArray = Array.from(posts.values());
if (postsArray.length === 0) {
console.error('❌ No posts found. Make sure you\'re on the correct profile page.');
return;
}
console.log(`\n✅ Analyzed ${postsArray.length} posts\n`);
console.log('═'.repeat(60));
console.log('📊 ENGAGEMENT SUMMARY');
console.log('═'.repeat(60));
// Calculate totals and averages
const totals = {
likes: postsArray.reduce((sum, p) => sum + p.likes, 0),
retweets: postsArray.reduce((sum, p) => sum + p.retweets, 0),
replies: postsArray.reduce((sum, p) => sum + p.replies, 0),
views: postsArray.reduce((sum, p) => sum + p.views, 0)
};
const averages = {
likes: totals.likes / postsArray.length,
retweets: totals.retweets / postsArray.length,
replies: totals.replies / postsArray.length,
views: totals.views / postsArray.length
};
const formatNum = (n) => {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return Math.round(n).toString();
};
console.log(`\n📈 TOTALS (${postsArray.length} posts):`);
console.log(` ❤️ Likes: ${formatNum(totals.likes)}`);
console.log(` 🔄 Retweets: ${formatNum(totals.retweets)}`);
console.log(` 💬 Replies: ${formatNum(totals.replies)}`);
console.log(` 👁️ Views: ${formatNum(totals.views)}`);
console.log(`\n📊 AVERAGES PER POST:`);
console.log(` ❤️ Likes: ${averages.likes.toFixed(1)}`);
console.log(` 🔄 Retweets: ${averages.retweets.toFixed(1)}`);
console.log(` 💬 Replies: ${averages.replies.toFixed(1)}`);
console.log(` 👁️ Views: ${formatNum(averages.views)}`);
// Engagement rate
if (totals.views > 0) {
const engagementRate = ((totals.likes + totals.retweets + totals.replies) / totals.views) * 100;
console.log(`\n📐 ENGAGEMENT RATE: ${engagementRate.toFixed(2)}%`);
console.log(` (Likes + Retweets + Replies) / Views`);
}
// Top performing posts
console.log('\n' + '═'.repeat(60));
console.log('🏆 TOP PERFORMING POSTS');
console.log('═'.repeat(60));
// By likes
const topLikes = [...postsArray].sort((a, b) => b.likes - a.likes).slice(0, 5);
console.log('\n❤️ TOP BY LIKES:');
topLikes.forEach((p, i) => {
console.log(` ${i + 1}. ${formatNum(p.likes)} likes — "${p.text.slice(0, 50)}..."`);
console.log(` ${p.url}`);
});
// By retweets
const topRetweets = [...postsArray].sort((a, b) => b.retweets - a.retweets).slice(0, 5);
console.log('\n🔄 TOP BY RETWEETS:');
topRetweets.forEach((p, i) => {
console.log(` ${i + 1}. ${formatNum(p.retweets)} RTs — "${p.text.slice(0, 50)}..."`);
console.log(` ${p.url}`);
});
// By engagement rate (if views available)
const postsWithViews = postsArray.filter(p => p.views > 0);
if (postsWithViews.length > 0) {
const topEngagement = postsWithViews
.map(p => ({ ...p, rate: ((p.likes + p.retweets + p.replies) / p.views) * 100 }))
.sort((a, b) => b.rate - a.rate)
.slice(0, 5);
console.log('\n📈 TOP BY ENGAGEMENT RATE:');
topEngagement.forEach((p, i) => {
console.log(` ${i + 1}. ${p.rate.toFixed(2)}% — "${p.text.slice(0, 50)}..."`);
console.log(` ${p.url}`);
});
}
// Worst performing
console.log('\n' + '═'.repeat(60));
console.log('📉 LOWEST PERFORMING (consider deleting or learning from)');
console.log('═'.repeat(60));
const bottomLikes = [...postsArray].sort((a, b) => a.likes - b.likes).slice(0, 3);
bottomLikes.forEach((p, i) => {
console.log(` ${i + 1}. ${p.likes} likes — "${p.text.slice(0, 50)}..."`);
console.log(` ${p.url}`);
});
// Save to localStorage
const storageKey = `xactions_engagement_${username}`;
const data = {
username,
timestamp: new Date().toISOString(),
postCount: postsArray.length,
totals,
averages,
posts: postsArray
};
localStorage.setItem(storageKey, JSON.stringify(data));
console.log('\n' + '═'.repeat(60));
console.log(`💾 Data saved! Run again to compare over time.`);
console.log(`📥 Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('═'.repeat(60) + '\n');
})();
});
register("filter-manager", function(){
(function filterManager() {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ⚙️ FILTER MANAGER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
const STORAGE_KEY = 'xactions_filters';
// Default filter configuration
const defaultFilters = {
// Follower counts
followers: {
min: 0,
max: Infinity,
enabled: false
},
// Following counts
following: {
min: 0,
max: Infinity,
enabled: false
},
// Follower/Following ratio
ratio: {
min: 0, // Minimum followers/following ratio
max: Infinity,
enabled: false
},
// Tweet count
tweets: {
min: 1,
max: Infinity,
enabled: false
},
// Account age (days)
accountAge: {
min: 30, // At least 30 days old
enabled: false
},
// Bio requirements
bio: {
required: false, // Must have bio
keywords: [], // Must contain these words
excludeKeywords: [], // Must NOT contain these
minLength: 0,
enabled: false
},
// Profile picture
profilePic: {
required: false, // Must have profile pic (not default)
enabled: false
},
// Language
language: {
allowed: [], // Empty = all languages
enabled: false
},
// Verified status
verified: {
skip: false, // Skip verified accounts
only: false, // Only verified accounts
enabled: false
},
// Activity
activity: {
lastTweetDays: 30, // Must have tweeted in last X days
enabled: false
},
// Spam detection
spam: {
skipNoTweets: true,
skipSuspicious: true, // Suspicious follower/following ratio
skipBotPatterns: true, // Username looks like bot
enabled: true
}
};
// Storage helpers
// Deep-clone so callers never mutate the shared defaults through the
// nested category objects a shallow spread would alias.
const getFilters = () => {
const base = structuredClone(defaultFilters);
try {
const data = localStorage.getItem(STORAGE_KEY);
if (data) Object.assign(base, JSON.parse(data));
} catch {
// Ignore corrupt stored config and fall back to defaults
}
// JSON cannot represent Infinity: stored max values round-trip as null,
// which would make every max comparison fail. Restore them.
Object.values(base).forEach(settings => {
if (settings && typeof settings === 'object' && 'max' in settings && settings.max == null) {
settings.max = Infinity;
}
});
return base;
};
const saveFilters = (filters) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(filters));
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Filters = {
// Get current filters
get: () => getFilters(),
// Set a specific filter
set: (category, key, value) => {
const filters = getFilters();
if (filters[category]) {
filters[category][key] = value;
saveFilters(filters);
console.log(`✅ Set ${category}.${key} = ${value}`);
} else {
console.error(`❌ Unknown filter category: ${category}`);
}
},
// Enable a filter category
enable: (category) => {
const filters = getFilters();
if (filters[category]) {
filters[category].enabled = true;
saveFilters(filters);
console.log(`✅ Enabled ${category} filter`);
}
},
// Disable a filter category
disable: (category) => {
const filters = getFilters();
if (filters[category]) {
filters[category].enabled = false;
saveFilters(filters);
console.log(`✅ Disabled ${category} filter`);
}
},
// Quick presets
presets: {
// Quality followers (engaged users)
quality: () => {
const filters = getFilters();
filters.followers.min = 100;
filters.followers.max = 50000;
filters.followers.enabled = true;
filters.tweets.min = 10;
filters.tweets.enabled = true;
filters.bio.required = true;
filters.bio.enabled = true;
filters.profilePic.required = true;
filters.profilePic.enabled = true;
filters.accountAge.min = 90;
filters.accountAge.enabled = true;
saveFilters(filters);
console.log('✅ Applied QUALITY preset');
},
// Influencers (high follower counts)
influencers: () => {
const filters = getFilters();
filters.followers.min = 10000;
filters.followers.max = Infinity;
filters.followers.enabled = true;
filters.verified.only = false;
filters.verified.enabled = false;
saveFilters(filters);
console.log('✅ Applied INFLUENCERS preset');
},
// Small accounts (easier to engage)
small: () => {
const filters = getFilters();
filters.followers.min = 50;
filters.followers.max = 5000;
filters.followers.enabled = true;
filters.activity.lastTweetDays = 7;
filters.activity.enabled = true;
saveFilters(filters);
console.log('✅ Applied SMALL ACCOUNTS preset');
},
// Anti-spam
antiSpam: () => {
const filters = getFilters();
filters.spam.skipNoTweets = true;
filters.spam.skipSuspicious = true;
filters.spam.skipBotPatterns = true;
filters.spam.enabled = true;
filters.tweets.min = 5;
filters.tweets.enabled = true;
filters.profilePic.required = true;
filters.profilePic.enabled = true;
saveFilters(filters);
console.log('✅ Applied ANTI-SPAM preset');
},
// No filters
none: () => {
saveFilters(defaultFilters);
console.log('✅ Reset to default (no filters)');
}
},
// Check if user passes all filters (helper for other scripts)
check: (userData) => {
const filters = getFilters();
const failures = [];
// Followers
if (filters.followers.enabled) {
if (userData.followers < filters.followers.min) {
failures.push(`followers < ${filters.followers.min}`);
}
if (userData.followers > filters.followers.max) {
failures.push(`followers > ${filters.followers.max}`);
}
}
// Following
if (filters.following.enabled) {
if (userData.following < filters.following.min) {
failures.push(`following < ${filters.following.min}`);
}
if (userData.following > filters.following.max) {
failures.push(`following > ${filters.following.max}`);
}
}
// Tweets
if (filters.tweets.enabled) {
if (userData.tweets < filters.tweets.min) {
failures.push(`tweets < ${filters.tweets.min}`);
}
}
// Bio
if (filters.bio.enabled && filters.bio.required && !userData.bio) {
failures.push('no bio');
}
// Profile pic
if (filters.profilePic.enabled && filters.profilePic.required && !userData.hasProfilePic) {
failures.push('no profile pic');
}
// Verified
if (filters.verified.enabled) {
if (filters.verified.skip && userData.isVerified) {
failures.push('is verified');
}
if (filters.verified.only && !userData.isVerified) {
failures.push('not verified');
}
}
// Follower/following ratio (declared in defaultFilters but never checked)
if (filters.ratio.enabled && userData.following > 0) {
const ratio = userData.followers / userData.following;
if (ratio < filters.ratio.min) failures.push(`ratio < ${filters.ratio.min}`);
if (ratio > filters.ratio.max) failures.push(`ratio > ${filters.ratio.max}`);
}
// Account age (declared in defaultFilters but never checked)
if (filters.accountAge.enabled && typeof userData.accountAgeDays === 'number') {
if (userData.accountAgeDays < filters.accountAge.min) {
failures.push(`account younger than ${filters.accountAge.min} days`);
}
}
// Language (declared in defaultFilters but never checked)
if (filters.language.enabled && filters.language.allowed.length > 0 && userData.lang) {
if (!filters.language.allowed.includes(userData.lang)) {
failures.push(`language "${userData.lang}" not allowed`);
}
}
// Activity / last tweet recency (declared in defaultFilters but never checked)
if (filters.activity.enabled && typeof userData.lastTweetDaysAgo === 'number') {
if (userData.lastTweetDaysAgo > filters.activity.lastTweetDays) {
failures.push(`inactive for ${userData.lastTweetDaysAgo} days`);
}
}
// Spam heuristics (declared in defaultFilters, enabled by default, but never checked)
if (filters.spam.enabled) {
if (filters.spam.skipNoTweets && userData.tweets === 0) {
failures.push('no tweets (spam heuristic)');
}
if (filters.spam.skipSuspicious && userData.following > 0) {
const followRatio = userData.following / (userData.followers || 1);
if (followRatio > 50) failures.push('suspicious following/followers ratio (spam heuristic)');
}
if (filters.spam.skipBotPatterns && userData.username && /\d{6,}$/.test(userData.username)) {
failures.push('username matches bot pattern (spam heuristic)');
}
}
return {
passes: failures.length === 0,
failures
};
},
// Show current configuration
show: () => {
const filters = getFilters();
console.log('');
console.log('═'.repeat(50));
console.log('⚙️ CURRENT FILTER CONFIGURATION');
console.log('═'.repeat(50));
Object.entries(filters).forEach(([category, settings]) => {
const status = settings.enabled ? '✅' : '⭕';
console.log(`${status} ${category}:`);
Object.entries(settings).forEach(([key, value]) => {
if (key !== 'enabled') {
console.log(` ${key}: ${JSON.stringify(value)}`);
}
});
});
console.log('═'.repeat(50));
console.log('');
},
// Reset to defaults
reset: () => {
if (confirm('⚠️ Reset all filters to defaults?')) {
saveFilters(defaultFilters);
console.log('✅ Filters reset to defaults.');
}
},
// Export
export: () => {
const filters = getFilters();
const json = JSON.stringify(filters, null, 2);
console.log('📋 Filter config (copy this):');
console.log(json);
navigator.clipboard?.writeText(json);
console.log('✅ Copied to clipboard!');
},
// Import
import: (jsonString) => {
try {
const imported = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
const merged = { ...getFilters(), ...imported };
saveFilters(merged);
console.log('✅ Filters imported.');
} catch (e) {
console.error('❌ Invalid JSON:', e.message);
}
},
// Help
help: () => {
console.log('');
console.log('📋 FILTER MANAGER COMMANDS:');
console.log('');
console.log(' XActions.Filters.show() - Show current config');
console.log(' XActions.Filters.get() - Get filters object');
console.log(' XActions.Filters.set(cat,key,val) - Set specific filter');
console.log(' XActions.Filters.enable("bio") - Enable filter');
console.log(' XActions.Filters.disable("bio") - Disable filter');
console.log('');
console.log('📦 PRESETS:');
console.log(' XActions.Filters.presets.quality()');
console.log(' XActions.Filters.presets.influencers()');
console.log(' XActions.Filters.presets.small()');
console.log(' XActions.Filters.presets.antiSpam()');
console.log(' XActions.Filters.presets.none()');
console.log('');
console.log('📤 EXPORT/IMPORT:');
console.log(' XActions.Filters.export()');
console.log(' XActions.Filters.import(json)');
console.log(' XActions.Filters.reset()');
console.log('');
}
};
const filters = getFilters();
const enabledCount = Object.values(filters).filter(f => f.enabled).length;
console.log(`⚙️ Filter Manager loaded! (${enabledCount} filters enabled)`);
console.log(' Run XActions.Filters.help() for commands.');
console.log(' Run XActions.Filters.show() to see current config.');
console.log('');
})();
});
register("find-fake-followers", function(){
var CONFIG = {
// Scroll settings
scrollDelay: 1500,
maxScrolls: 50,
maxRetries: 3,
// Scoring weights (higher = more suspicious)
scoring: {
// Following/Followers ratio
highFollowingRatio: 25, // Following 50x more than followers
veryHighFollowingRatio: 40, // Following 100x more than followers
// Account characteristics
defaultAvatar: 20, // Default profile picture
noBio: 15, // Empty bio
suspiciousBio: 25, // Suspicious keywords in bio
randomUsername: 15, // Username pattern like abc12345678
// Following count
massFollowing: 15, // Following > 3000
extremeFollowing: 25, // Following > 5000
// Follower count
noFollowers: 20, // 0 followers
veryFewFollowers: 10 // < 10 followers
},
// Thresholds
thresholds: {
highRatio: 50, // Following/Followers ratio
veryHighRatio: 100,
massFollowing: 3000,
extremeFollowing: 5000
},
// Suspicious bio keywords (from x-bot-sweeper)
suspiciousKeywords: [
'crypto', 'nft', 'bitcoin', 'btc', 'eth', 'forex', 'trading signals',
'giveaway', 'airdrop', 'free money', 'passive income',
'onlyfans', 'fansly', 'dm for', 'link in bio', 'check bio',
'follow back', 'f4f', 'follow4follow', 'followback',
'18+', 'adult', 'nsfw', 'sexy', 'hot girl',
'make money', 'work from home', 'get rich', 'financial freedom',
'investment opportunity', 'guaranteed returns'
],
// Minimum score to flag as fake
minFakeScore: 40,
// Likely fake threshold
likelyFakeScore: 60
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function findFakeFollowers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🕵️ XActions — Find Fake Followers ║
║ Identify likely fake/bot accounts in your audience ║
╚══════════════════════════════════════════════════════════════╝
`);
if (!window.location.pathname.includes('/followers')) {
console.error('❌ Please navigate to your FOLLOWERS page first!');
console.log('👉 Go to: https://x.com/YOUR_USERNAME/followers');
return;
}
const $userCell = '[data-testid="UserCell"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '');
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
const analyzeFollower = (cell) => {
const result = {
username: '',
displayName: '',
bio: '',
following: 0,
followers: 0,
score: 0,
flags: []
};
// Get username
const link = cell.querySelector('a[href^="/"]');
if (link) {
result.username = link.getAttribute('href').replace('/', '').split('/')[0];
}
// Get display name
const nameEl = cell.querySelector('[dir="ltr"] span');
result.displayName = nameEl?.textContent || result.username;
// Get bio
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
result.bio = bioEl?.textContent || '';
const bioLower = result.bio.toLowerCase();
// Parse stats from cell text
const text = cell.textContent;
const followingMatch = text.match(/([\d,.]+[KMB]?)\s*Following/i);
const followersMatch = text.match(/([\d,.]+[KMB]?)\s*Follower/i);
if (followingMatch) result.following = parseCount(followingMatch[1]);
if (followersMatch) result.followers = parseCount(followersMatch[1]);
// === SCORING ===
// Check ratio
if (result.following > 0 && result.followers > 0) {
const ratio = result.following / result.followers;
if (ratio > CONFIG.thresholds.veryHighRatio) {
result.score += CONFIG.scoring.veryHighFollowingRatio;
result.flags.push(`Extreme ratio (${ratio.toFixed(0)}:1)`);
} else if (ratio > CONFIG.thresholds.highRatio) {
result.score += CONFIG.scoring.highFollowingRatio;
result.flags.push(`High ratio (${ratio.toFixed(0)}:1)`);
}
}
// Check following count
if (result.following > CONFIG.thresholds.extremeFollowing) {
result.score += CONFIG.scoring.extremeFollowing;
result.flags.push(`Mass following (${result.following.toLocaleString()})`);
} else if (result.following > CONFIG.thresholds.massFollowing) {
result.score += CONFIG.scoring.massFollowing;
result.flags.push(`High following (${result.following.toLocaleString()})`);
}
// Check follower count (only when the count was actually present in the
// cell; a failed parse defaults to 0 and must not read as "zero followers")
if (followersMatch) {
if (result.followers === 0) {
result.score += CONFIG.scoring.noFollowers;
result.flags.push('Zero followers');
} else if (result.followers < 10) {
result.score += CONFIG.scoring.veryFewFollowers;
result.flags.push(`Very few followers (${result.followers})`);
}
}
// Check for default avatar
const avatar = cell.querySelector('img[src*="default_profile"]');
if (avatar) {
result.score += CONFIG.scoring.defaultAvatar;
result.flags.push('Default avatar');
}
// Check for no bio
if (!result.bio.trim()) {
result.score += CONFIG.scoring.noBio;
result.flags.push('No bio');
}
// Check for suspicious bio keywords
const foundKeywords = CONFIG.suspiciousKeywords.filter(kw => bioLower.includes(kw.toLowerCase()));
if (foundKeywords.length > 0) {
result.score += CONFIG.scoring.suspiciousBio;
result.flags.push(`Suspicious bio: ${foundKeywords.slice(0, 3).join(', ')}`);
}
// Check for random username pattern
const numMatch = result.username.match(/\d+/g);
if (numMatch && numMatch.join('').length >= 8) {
result.score += CONFIG.scoring.randomUsername;
result.flags.push('Random username pattern');
}
return result;
};
console.log('🔍 Scanning followers for fake accounts...\n');
console.log('Bot detection criteria from x-bot-sweeper:');
console.log(' • High following/followers ratio');
console.log(' • Default profile picture');
console.log(' • Suspicious bio keywords');
console.log(' • Very new or inactive accounts');
console.log('');
const scanned = new Set();
const allFollowers = [];
let retries = 0;
let scrollCount = 0;
while (scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = scanned.size;
document.querySelectorAll($userCell).forEach(cell => {
const analysis = analyzeFollower(cell);
if (!analysis.username || scanned.has(analysis.username)) return;
scanned.add(analysis.username);
allFollowers.push(analysis);
});
if (scanned.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scanned.size} followers...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
if (allFollowers.length === 0) {
console.error('❌ No followers found. Make sure the follower list has loaded.');
return;
}
// Categorize
const likelyFake = allFollowers.filter(f => f.score >= CONFIG.likelyFakeScore);
const suspicious = allFollowers.filter(f => f.score >= CONFIG.minFakeScore && f.score < CONFIG.likelyFakeScore);
const clean = allFollowers.filter(f => f.score < CONFIG.minFakeScore);
const fakePercentage = ((likelyFake.length + suspicious.length) / allFollowers.length * 100).toFixed(1);
console.log(`\n✅ Analysis complete!\n`);
console.log('═'.repeat(60));
console.log('📊 FAKE FOLLOWER ANALYSIS');
console.log('═'.repeat(60));
console.log('\n📈 SUMMARY:');
console.log('─'.repeat(50));
console.log(` Total followers scanned: ${allFollowers.length}`);
console.log(` 🔴 Likely fake/bot: ${likelyFake.length} (${(likelyFake.length/allFollowers.length*100).toFixed(1)}%)`);
console.log(` 🟡 Suspicious: ${suspicious.length} (${(suspicious.length/allFollowers.length*100).toFixed(1)}%)`);
console.log(` 🟢 Appear legitimate: ${clean.length} (${(clean.length/allFollowers.length*100).toFixed(1)}%)`);
console.log(`\n 📊 Estimated fake rate: ${fakePercentage}%`);
// Quality assessment
console.log('\n🏆 FOLLOWER QUALITY ASSESSMENT:');
console.log('─'.repeat(50));
if (parseFloat(fakePercentage) < 10) {
console.log(' ✅ EXCELLENT - Your audience appears very genuine!');
} else if (parseFloat(fakePercentage) < 25) {
console.log(' 👍 GOOD - Some suspicious accounts, but mostly clean');
} else if (parseFloat(fakePercentage) < 50) {
console.log(' ⚠️ MODERATE - Consider cleaning up fake followers');
} else {
console.log(' 🚨 POOR - High percentage of likely fake followers');
}
// Likely fake accounts
if (likelyFake.length > 0) {
console.log('\n' + '═'.repeat(60));
console.log('🔴 LIKELY FAKE/BOT ACCOUNTS');
console.log('═'.repeat(60));
likelyFake.sort((a, b) => b.score - a.score).slice(0, 30).forEach((f, i) => {
console.log(`\n${i + 1}. @${f.username} (Score: ${f.score})`);
console.log(` ${f.displayName}`);
console.log(` Flags: ${f.flags.join(' | ')}`);
console.log(` https://x.com/${f.username}`);
});
if (likelyFake.length > 30) {
console.log(`\n... and ${likelyFake.length - 30} more likely fake accounts`);
}
}
// Suspicious accounts
if (suspicious.length > 0) {
console.log('\n' + '═'.repeat(60));
console.log('🟡 SUSPICIOUS ACCOUNTS (review manually)');
console.log('═'.repeat(60));
suspicious.sort((a, b) => b.score - a.score).slice(0, 15).forEach((f, i) => {
console.log(`\n${i + 1}. @${f.username} (Score: ${f.score})`);
console.log(` Flags: ${f.flags.join(' | ')}`);
});
if (suspicious.length > 15) {
console.log(`\n... and ${suspicious.length - 15} more suspicious accounts`);
}
}
// Common flags
console.log('\n' + '═'.repeat(60));
console.log('📊 MOST COMMON RED FLAGS');
console.log('═'.repeat(60));
const flagCounts = {};
[...likelyFake, ...suspicious].forEach(f => {
f.flags.forEach(flag => {
const category = flag.split('(')[0].trim().split(':')[0].trim();
flagCounts[category] = (flagCounts[category] || 0) + 1;
});
});
Object.entries(flagCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([flag, count]) => {
const pct = (count / (likelyFake.length + suspicious.length) * 100).toFixed(0);
console.log(` ${flag}: ${count} accounts (${pct}%)`);
});
// Save results
const storageKey = 'xactions_fake_followers';
const data = {
timestamp: new Date().toISOString(),
totalScanned: allFollowers.length,
likelyFake: likelyFake.map(f => ({ username: f.username, score: f.score, flags: f.flags })),
suspicious: suspicious.map(f => ({ username: f.username, score: f.score, flags: f.flags })),
fakePercentage: parseFloat(fakePercentage)
};
localStorage.setItem(storageKey, JSON.stringify(data));
console.log('\n' + '═'.repeat(60));
console.log('💡 RECOMMENDED ACTIONS');
console.log('═'.repeat(60));
console.log('\n1. Review the "Likely Fake" accounts and block obvious bots');
console.log('2. Use the block-bots.js script to automate blocking');
console.log('3. Report accounts that are clearly spam');
console.log('4. Run this audit periodically to maintain quality');
console.log('\n' + '═'.repeat(60));
console.log(`💾 Results saved! Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('═'.repeat(60) + '\n');
})();
});
register("follow-back", function(){
(async function followBack() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Hard cap on follow-backs this run
maxFollows: 50,
// Skip verified (blue check) accounts
skipVerified: false,
// Skip protected/locked accounts
skipProtected: false,
// Timing (randomized between min and max, per follow)
minDelay: 2000,
maxDelay: 5000,
// Scrolling to load more followers
maxScrollRounds: 40,
scrollDelay: 2000,
maxEmptyScrolls: 6
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
followButton: '[data-testid$="-follow"]', // ends with -follow (not -unfollow)
unfollowButton: '[data-testid$="-unfollow"]',
followsYou: '[data-testid="userFollowIndicator"]',
verified: '[data-testid="icon-verified"]',
protectedIcon: '[data-testid="icon-lock"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const getUsername = (cell) => {
const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
if (!link) return null;
const m = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
if (!m) return null;
if (['home', 'explore', 'notifications', 'messages', 'i', 'search', 'settings'].includes(m[1])) return null;
return m[1];
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} followed back`)
};
// ============================================
// 🎯 STATE
// ============================================
const stats = { followed: 0, alreadyFollowing: 0, notMutual: 0, skippedVerified: 0, skippedProtected: 0, errors: 0 };
const processed = new Set();
// Stop switch: run window.stopFollowBack() to abort after the current account.
let stopped = false;
window.stopFollowBack = () => {
stopped = true;
log.warning('Stop requested. Finishing the current account, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔁 FOLLOW BACK - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
log.info('To stop early: window.stopFollowBack()');
// Page guard
if (!/\/followers/.test(window.location.pathname)) {
log.error('Not on a followers page. Go to https://x.com/<your-username>/followers and re-run.');
return stats;
}
log.info(`Max follow-backs: ${CONFIG.maxFollows}`);
log.info(`Skip verified: ${CONFIG.skipVerified} | Skip protected: ${CONFIG.skipProtected}`);
// ============================================
// 🔁 MAIN LOOP
// ============================================
let scrollRounds = 0;
let emptyScrolls = 0;
while (!stopped && stats.followed < CONFIG.maxFollows && scrollRounds < CONFIG.maxScrollRounds && emptyScrolls < CONFIG.maxEmptyScrolls) {
const cells = document.querySelectorAll(SELECTORS.userCell);
if (cells.length === 0) {
log.warning('No follower rows visible yet. Scrolling...');
}
let foundNew = false;
for (const cell of cells) {
if (stopped || stats.followed >= CONFIG.maxFollows) break;
const username = getUsername(cell);
if (!username) continue;
const key = username.toLowerCase();
if (processed.has(key)) continue;
processed.add(key);
foundNew = true;
try {
// Must follow you.
if (!cell.querySelector(SELECTORS.followsYou)) {
stats.notMutual++;
continue;
}
// Already following? A -unfollow button means you follow them.
if (cell.querySelector(SELECTORS.unfollowButton)) {
stats.alreadyFollowing++;
continue;
}
if (CONFIG.skipVerified && cell.querySelector(SELECTORS.verified)) {
stats.skippedVerified++;
continue;
}
if (CONFIG.skipProtected && cell.querySelector(SELECTORS.protectedIcon)) {
stats.skippedProtected++;
continue;
}
const followBtn = cell.querySelector(SELECTORS.followButton);
if (!followBtn) continue;
cell.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
followBtn.click();
stats.followed++;
log.success(`Followed back #${stats.followed}: @${username}`);
log.progress(stats.followed, CONFIG.maxFollows);
await randomDelay();
} catch (error) {
log.error(`Error on @${username}: ${error.message}`);
stats.errors++;
}
}
if (!foundNew) emptyScrolls++;
else emptyScrolls = 0;
window.scrollTo(0, document.body.scrollHeight);
scrollRounds++;
await sleep(CONFIG.scrollDelay);
}
if (emptyScrolls >= CONFIG.maxEmptyScrolls) {
log.info('Reached the end of the followers list.');
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 FOLLOW BACK - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Followed back: ${String(stats.followed).padEnd(32)}║
║ 🤝 Already following: ${String(stats.alreadyFollowing).padEnd(32)}║
║ 🚫 Not mutual: ${String(stats.notMutual).padEnd(32)}║
║ ✔️ Skipped verified: ${String(stats.skippedVerified).padEnd(32)}║
║ 🔒 Skipped protected: ${String(stats.skippedProtected).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("follow-engagers", function(){
var CONFIG = {
// ---- ENGAGEMENT TYPE ----
// Which engagers to follow
// Options: 'likers', 'retweeters', 'all'
mode: 'likers',
// ---- LIMITS ----
// Max follows from this post
maxFollows: 20,
// ---- FILTERS ----
filters: {
// Minimum followers the user must have
minFollowers: 100,
// Maximum followers (to avoid following huge accounts)
maxFollowers: 50000,
// Skip protected/private accounts
skipProtected: true,
// Skip verified accounts
skipVerified: false
},
// ---- TIMING ----
// Delay between follows (milliseconds)
minDelay: 2000,
maxDelay: 4000,
// Scroll delay
scrollDelay: 1500
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function followEngagers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
// DOM Selectors
const $userCell = '[data-testid="UserCell"]';
const $followButton = '[data-testid$="-follow"]';
const $protectedIcon = '[data-testid="icon-lock"]';
const $verifiedBadge = '[data-testid="icon-verified"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 👥 FOLLOW ENGAGERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on a tweet page
if (!window.location.href.includes('/status/')) {
console.error('❌ ERROR: Must be on a tweet page!');
console.log('📍 Go to any tweet: https://x.com/user/status/TWEET_ID');
return;
}
const tweetUrl = window.location.href.split('?')[0];
console.log(`📍 Tweet: ${tweetUrl}`);
console.log(`🎯 Mode: ${CONFIG.mode}`);
console.log(`📊 Max follows: ${CONFIG.maxFollows}`);
console.log('');
const STORAGE_KEY = 'xactions_followed_engagers';
const followedUsers = new Set();
// Load previously followed
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) JSON.parse(saved).forEach(u => followedUsers.add(u));
} catch (e) {}
let totalFollowed = 0;
/**
* Get username from user cell
*/
function getUsername(cell) {
const link = cell.querySelector('a[href^="/"]');
return link ? link.getAttribute('href').replace('/', '').split('/')[0] : null;
}
/**
* Check if user passes filters
*/
function passesFilters(cell) {
// Check protected
if (CONFIG.filters.skipProtected && cell.querySelector($protectedIcon)) {
return false;
}
// Check verified
if (CONFIG.filters.skipVerified && cell.querySelector($verifiedBadge)) {
return false;
}
// Note: Follower count filtering would require visiting each profile
// For simplicity, we skip that in the console version
return true;
}
/**
* Scrape and follow users from current list
*/
async function followFromList() {
let scrolls = 0;
const maxScrolls = 30;
while (totalFollowed < CONFIG.maxFollows && scrolls < maxScrolls) {
const cells = document.querySelectorAll($userCell);
for (const cell of cells) {
if (totalFollowed >= CONFIG.maxFollows) break;
const username = getUsername(cell);
if (!username || followedUsers.has(username)) continue;
// Check filters
if (!passesFilters(cell)) {
console.log(`⏭️ Skipping @${username} (filtered)`);
continue;
}
// Find follow button
const followBtn = cell.querySelector($followButton);
if (!followBtn) continue; // Already following or no button
try {
followBtn.click();
followedUsers.add(username);
totalFollowed++;
console.log(`✅ Followed #${totalFollowed}: @${username}`);
// Save to storage
localStorage.setItem(STORAGE_KEY, JSON.stringify([...followedUsers]));
await sleep(randomDelay());
} catch (e) {
console.warn('⚠️ Error following:', e.message);
}
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
}
// Navigate to likes or retweets list
if (CONFIG.mode === 'likers' || CONFIG.mode === 'all') {
console.log('📍 Opening likes list...');
// Find likes link
const likesLink = document.querySelector('a[href$="/likes"]');
if (likesLink) {
likesLink.click();
await sleep(2000);
console.log('🚀 Following likers...');
await followFromList();
} else {
console.log('⚠️ Could not find likes link. Try clicking on the likes count manually.');
}
}
if (CONFIG.mode === 'retweeters' || CONFIG.mode === 'all') {
// Go back if needed
if (CONFIG.mode === 'all') {
window.history.back();
await sleep(2000);
}
console.log('📍 Opening retweets list...');
const retweetsLink = document.querySelector('a[href$="/retweets"]');
if (retweetsLink) {
retweetsLink.click();
await sleep(2000);
console.log('🚀 Following retweeters...');
await followFromList();
} else {
console.log('⚠️ Could not find retweets link.');
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ FOLLOW ENGAGERS COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`👥 Total followed: ${totalFollowed}`);
console.log('');
return { followed: totalFollowed };
})();
});
register("follow-list-members", function(){
(async function followListMembers() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of members to follow this run
maxFollows: 50,
// Maximum scroll attempts before giving up
maxScrollAttempts: 100,
// Stop after this many scrolls with no new members (end of list)
noNewThreshold: 5,
// Skip verified accounts
skipVerified: false,
// Skip protected (private) accounts
skipProtected: false,
// Rate limiting between follows (randomized between min and max, ms)
minDelay: 2000,
maxDelay: 5000,
// Delay between scrolls (ms)
scrollDelay: 2000
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
// Follow button ends with -follow (id contains the user id); unfollow ends with -unfollow
followButton: '[data-testid$="-follow"]',
unfollowButton: '[data-testid$="-unfollow"]',
verifiedBadge: '[data-testid="icon-verified"]',
protectedIcon: '[data-testid="icon-lock"]',
profileLink: 'a[href^="/"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const handleFromCell = (cell) => {
const link = cell.querySelector(SELECTORS.profileLink);
if (!link) return '';
const href = link.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
return m ? m[1] : '';
};
// ============================================
// 🎯 MAIN
// ============================================
let stopped = false;
window.stopFollowListMembers = () => {
stopped = true;
log.warning('Stop requested. Finishing the current member, then exiting.');
};
const stats = {
followed: 0,
alreadyFollowing: 0,
skippedVerified: 0,
skippedProtected: 0,
errors: 0
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 👥 FOLLOW LIST MEMBERS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: must be on a list members page
if (!/\/lists\/[^/]+\/members/.test(window.location.pathname)) {
log.warning('You are not on a List members page.');
log.info('Open https://x.com/i/lists/<list-id>/members (or add /members to a list URL), then rerun.');
return;
}
log.info(`Max follows: ${CONFIG.maxFollows}`);
log.info(`Skip verified: ${CONFIG.skipVerified}, skip protected: ${CONFIG.skipProtected}`);
log.info('To stop early: window.stopFollowListMembers()');
console.log('');
const processed = new Set();
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && stats.followed < CONFIG.maxFollows && scrollAttempts < CONFIG.maxScrollAttempts) {
const cells = document.querySelectorAll(SELECTORS.userCell);
let foundNew = false;
for (const cell of cells) {
if (stopped || stats.followed >= CONFIG.maxFollows) break;
const handle = handleFromCell(cell);
const key = handle || (cell.textContent || '').slice(0, 40);
if (processed.has(key)) continue;
processed.add(key);
foundNew = true;
try {
// Already following? The cell shows an unfollow-style button.
if (cell.querySelector(SELECTORS.unfollowButton)) {
stats.alreadyFollowing++;
continue;
}
if (CONFIG.skipVerified && cell.querySelector(SELECTORS.verifiedBadge)) {
stats.skippedVerified++;
continue;
}
if (CONFIG.skipProtected && cell.querySelector(SELECTORS.protectedIcon)) {
stats.skippedProtected++;
continue;
}
const followBtn = cell.querySelector(SELECTORS.followButton);
if (!followBtn) continue;
cell.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
followBtn.click();
stats.followed++;
log.success(`Followed #${stats.followed}: @${handle || 'unknown'}`);
await sleep(randomDelay());
} catch (e) {
stats.errors++;
log.error(`Error following @${handle || 'unknown'}: ${e.message}`);
}
}
if (!foundNew) {
noNewCount++;
if (noNewCount >= CONFIG.noNewThreshold) {
log.warning('No new members after several scrolls. Reached the end of the list.');
break;
}
} else {
noNewCount = 0;
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ 📊 FOLLOW LIST MEMBERS - COMPLETE ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log(` ✅ Followed: ${stats.followed}`);
console.log(` 💚 Already following: ${stats.alreadyFollowing}`);
console.log(` ⏭️ Skipped verified: ${stats.skippedVerified}`);
console.log(` 🔒 Skipped protected: ${stats.skippedProtected}`);
console.log(` ❌ Errors: ${stats.errors}`);
console.log(` 📜 Scroll attempts: ${scrollAttempts}`);
if (stats.followed === 0 && stats.alreadyFollowing === 0) {
log.warning('No member cells were processed. The list may be empty or still loading.');
}
console.log('✅ Script completed! by nichxbt');
return stats;
})();
});
register("follow-target-users", function(){
var CONFIG = {
// ---- LIMITS ----
// Maximum users to follow
maxFollows: 30,
// Max scrolls to load users
maxScrolls: 50,
// ---- FILTERS ----
filters: {
// Skip protected/private accounts
skipProtected: true,
// Skip verified accounts
skipVerified: false,
// Only follow users with bio containing these keywords
// 💡 Leave empty [] to follow anyone
bioKeywords: [],
// Skip users whose bio contains these words
bioBlacklist: ['bot', 'spam', 'promo']
},
// ---- TIMING ----
minDelay: 2000,
maxDelay: 4000,
scrollDelay: 1500
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function followTargetUsers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
const $userCell = '[data-testid="UserCell"]';
const $followButton = '[data-testid$="-follow"]';
const $protectedIcon = '[data-testid="icon-lock"]';
const $verifiedBadge = '[data-testid="icon-verified"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎯 FOLLOW TARGET USERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify page
const url = window.location.href;
const pathMatch = url.match(/x\.com\/([^\/]+)\/(followers|following)/);
if (!pathMatch) {
console.error('❌ ERROR: Must be on a followers or following page!');
console.log('📍 Go to: https://x.com/TARGET/followers');
return;
}
const targetAccount = pathMatch[1];
const listType = pathMatch[2];
console.log(`👤 Target account: @${targetAccount}`);
console.log(`📋 List type: ${listType}`);
console.log(`📊 Max follows: ${CONFIG.maxFollows}`);
console.log('');
const STORAGE_KEY = 'xactions_followed_targets';
const followedUsers = new Set();
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) JSON.parse(saved).forEach(u => followedUsers.add(u));
} catch (e) {}
let totalFollowed = 0;
let scrolls = 0;
/**
* Get user info from cell
*/
function getUserInfo(cell) {
const link = cell.querySelector('a[href^="/"]');
const username = link ? link.getAttribute('href').replace('/', '').split('/')[0] : null;
// Get bio text
const bioEl = cell.querySelector('[dir="auto"]:not([role])');
const bio = bioEl ? bioEl.innerText.toLowerCase() : '';
return { username, bio };
}
/**
* Check if user passes filters
*/
function passesFilters(cell, bio) {
// Check protected
if (CONFIG.filters.skipProtected && cell.querySelector($protectedIcon)) {
return false;
}
// Check verified
if (CONFIG.filters.skipVerified && cell.querySelector($verifiedBadge)) {
return false;
}
// Bio keywords whitelist
if (CONFIG.filters.bioKeywords.length > 0) {
const hasKeyword = CONFIG.filters.bioKeywords.some(k => bio.includes(k.toLowerCase()));
if (!hasKeyword) return false;
}
// Bio blacklist
if (CONFIG.filters.bioBlacklist.length > 0) {
const hasBlacklist = CONFIG.filters.bioBlacklist.some(k => bio.includes(k.toLowerCase()));
if (hasBlacklist) return false;
}
return true;
}
console.log('🚀 Starting to follow users...');
console.log('');
while (totalFollowed < CONFIG.maxFollows && scrolls < CONFIG.maxScrolls) {
const cells = document.querySelectorAll($userCell);
for (const cell of cells) {
if (totalFollowed >= CONFIG.maxFollows) break;
const { username, bio } = getUserInfo(cell);
if (!username || followedUsers.has(username)) continue;
// Check filters
if (!passesFilters(cell, bio)) {
continue;
}
// Find follow button
const followBtn = cell.querySelector($followButton);
if (!followBtn) continue;
try {
followBtn.click();
followedUsers.add(username);
totalFollowed++;
console.log(`✅ Followed #${totalFollowed}: @${username}`);
localStorage.setItem(STORAGE_KEY, JSON.stringify([...followedUsers]));
await sleep(randomDelay());
} catch (e) {
console.warn('⚠️ Error:', e.message);
}
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
if (scrolls % 10 === 0) {
console.log(`📜 Scrolled ${scrolls} times, followed ${totalFollowed}...`);
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ FOLLOW TARGET USERS COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`👥 Total followed: ${totalFollowed}`);
console.log('');
return { followed: totalFollowed };
})();
});
register("followers-growth-tracker", function(){
var CONFIG = {
// Storage key prefix
storageKey: 'xactions_growth_tracker',
// Maximum history entries to keep
maxHistory: 365,
// Show chart in console
showChart: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function followersGrowthTracker() {
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 📈 XActions — Followers Growth Tracker ║
║ Track follower growth over time ║
╚══════════════════════════════════════════════════════════════╝
`);
// Get username from URL
const pathMatch = window.location.pathname.match(/^\/([^\/]+)/);
if (!pathMatch) {
console.error('❌ Please navigate to a profile page first!');
console.log('👉 Example: https://x.com/elonmusk');
return;
}
const username = pathMatch[1].toLowerCase();
// Skip non-profile pages
const invalidPaths = ['home', 'explore', 'notifications', 'messages', 'i', 'settings', 'search'];
if (invalidPaths.includes(username)) {
console.error('❌ Please navigate to a profile page first!');
return;
}
console.log(`📊 Tracking growth for @${username}\n`);
// Extract follower/following counts from the page
const getStats = () => {
const stats = { followers: 0, following: 0 };
// Try to find follower count
const links = document.querySelectorAll('a[href$="/verified_followers"], a[href$="/followers"]');
links.forEach(link => {
const text = link.textContent;
const match = text.match(/([\d,.]+[KMB]?)\s*Followers/i);
if (match) {
stats.followers = parseCount(match[1]);
}
});
// Try to find following count
const followingLinks = document.querySelectorAll('a[href$="/following"]');
followingLinks.forEach(link => {
const text = link.textContent;
const match = text.match(/([\d,.]+[KMB]?)\s*Following/i);
if (match) {
stats.following = parseCount(match[1]);
}
});
// Alternative: look for span elements with counts
if (stats.followers === 0) {
document.querySelectorAll('span').forEach(span => {
const text = span.textContent;
if (text.match(/^\d[\d,.]*[KMB]?$/) && span.closest('a[href*="followers"]')) {
stats.followers = parseCount(text);
}
if (text.match(/^\d[\d,.]*[KMB]?$/) && span.closest('a[href*="following"]')) {
stats.following = parseCount(text);
}
});
}
return stats;
};
const parseCount = (str) => {
str = str.replace(/,/g, '');
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
if (match[2]) {
num *= multipliers[match[2].toUpperCase()];
}
return Math.round(num);
}
return parseInt(str) || 0;
};
const storageKey = `${CONFIG.storageKey}_${username}`;
const loadHistory = () => {
try {
const data = localStorage.getItem(storageKey);
return data ? JSON.parse(data) : [];
} catch (e) {
return [];
}
};
const saveHistory = (history) => {
// Keep only last N entries
const trimmed = history.slice(-CONFIG.maxHistory);
localStorage.setItem(storageKey, JSON.stringify(trimmed));
};
const formatDate = (iso) => {
const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
const formatNumber = (n) => {
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return n.toString();
};
// Get current stats
const currentStats = getStats();
if (currentStats.followers === 0) {
console.warn('⚠️ Could not detect follower count. Make sure you\'re on a profile page.');
console.log(' Try scrolling up to make sure the profile header is visible.');
return;
}
console.log(`📌 Current Stats:`);
console.log(` Followers: ${formatNumber(currentStats.followers)}`);
console.log(` Following: ${formatNumber(currentStats.following)}`);
console.log(` Ratio: ${(currentStats.followers / (currentStats.following || 1)).toFixed(2)}`);
// Load history and add new entry
const history = loadHistory();
const now = new Date().toISOString();
const today = now.split('T')[0];
// Check if we already have an entry for today
const todayIndex = history.findIndex(h => h.date.startsWith(today));
const entry = {
date: now,
followers: currentStats.followers,
following: currentStats.following
};
if (todayIndex >= 0) {
history[todayIndex] = entry;
console.log('\n📝 Updated today\'s entry');
} else {
history.push(entry);
console.log('\n📝 Added new entry');
}
saveHistory(history);
// Calculate growth metrics
if (history.length >= 2) {
console.log('\n📈 GROWTH ANALYSIS:\n');
console.log('─'.repeat(50));
const sorted = [...history].sort((a, b) => new Date(a.date) - new Date(b.date));
const oldest = sorted[0];
const newest = sorted[sorted.length - 1];
const daysDiff = Math.max(1, Math.round((new Date(newest.date) - new Date(oldest.date)) / (1000 * 60 * 60 * 24)));
const totalGrowth = newest.followers - oldest.followers;
const dailyAvg = totalGrowth / daysDiff;
const weeklyProjection = dailyAvg * 7;
const monthlyProjection = dailyAvg * 30;
console.log(`📅 Tracking period: ${daysDiff} days`);
console.log(` From: ${formatDate(oldest.date)} (${formatNumber(oldest.followers)} followers)`);
console.log(` To: ${formatDate(newest.date)} (${formatNumber(newest.followers)} followers)`);
console.log('');
console.log(`📊 Total growth: ${totalGrowth >= 0 ? '+' : ''}${formatNumber(totalGrowth)} followers`);
console.log(` Daily average: ${dailyAvg >= 0 ? '+' : ''}${dailyAvg.toFixed(1)} followers/day`);
console.log(` Weekly projection: ${weeklyProjection >= 0 ? '+' : ''}${formatNumber(Math.round(weeklyProjection))}`);
console.log(` Monthly projection: ${monthlyProjection >= 0 ? '+' : ''}${formatNumber(Math.round(monthlyProjection))}`);
// Recent changes
if (sorted.length >= 2) {
const prev = sorted[sorted.length - 2];
const recentChange = newest.followers - prev.followers;
console.log(`\n🔄 Since last check (${formatDate(prev.date)}):`);
console.log(` ${recentChange >= 0 ? '+' : ''}${recentChange} followers`);
}
// ASCII chart
if (CONFIG.showChart && sorted.length >= 3) {
console.log('\n📉 Growth Chart (last 14 days):\n');
const recent = sorted.slice(-14);
const values = recent.map(h => h.followers);
const min = Math.min(...values);
const max = Math.max(...values);
const range = max - min || 1;
const height = 8;
// Build chart
const chart = [];
for (let row = height; row >= 0; row--) {
let line = '';
const threshold = min + (range * row / height);
if (row === height) {
line = formatNumber(max).padStart(6) + ' ┤';
} else if (row === 0) {
line = formatNumber(min).padStart(6) + ' ┤';
} else {
line = ' ┤';
}
recent.forEach((h, i) => {
const normalized = (h.followers - min) / range * height;
if (normalized >= row) {
line += '█';
} else {
line += ' ';
}
});
chart.push(line);
}
chart.push(' └' + '─'.repeat(recent.length));
// One column per entry: a 3-char label every 3rd entry lines up with its column
chart.push(' ' + recent.map((h, i) => i % 3 === 0 ? formatDate(h.date).slice(0, 3).padEnd(3) : '').join('').slice(0, recent.length + 2));
chart.forEach(line => console.log(line));
}
console.log('\n' + '─'.repeat(50));
} else {
console.log('\n📸 First snapshot saved!');
console.log(' Run this script again later to see growth metrics.');
}
console.log(`\n💾 History saved (${history.length} entries)`);
console.log(' Run this script periodically to track growth over time.\n');
// Export data option
console.log('📥 To export your data, run: copy(localStorage.getItem("' + storageKey + '"))');
})();
});
register("growth-suite", function(){
var CONFIG = {
// ============================================
// TARGETING
// ============================================
// Keywords to search for (niche targeting)
keywords: [
'web3 developer',
'crypto trader',
'NFT artist',
],
// Accounts to engage with (their followers are targets)
targetAccounts: [
// 'vitalikbuterin',
// 'elonmusk',
],
// ============================================
// ACTIONS
// ============================================
actions: {
follow: true, // Follow users from searches
like: true, // Like posts in feed
unfollow: true, // Unfollow non-followers after grace period
},
// ============================================
// LIMITS (per session)
// ============================================
limits: {
follows: 20,
likes: 30,
unfollows: 15,
},
// ============================================
// TIMING
// ============================================
timing: {
unfollowAfterDays: 3, // Days before unfollowing
delayBetweenActions: 3000, // ms between actions
sessionDuration: 30, // minutes
},
// ============================================
// FILTERS
// ============================================
filters: {
minFollowers: 50,
maxFollowers: 50000,
mustHaveBio: true,
skipPrivate: true,
language: null, // null = any, or 'en', 'es', etc.
},
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function growthSuite() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = (min, max) => sleep(Math.random() * (max - min) + min);
// DOM Selectors
const SELECTORS = {
followButton: '[data-testid$="-follow"]',
unfollowButton: '[data-testid$="-unfollow"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
tweet: '[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userCell: '[data-testid="UserCell"]',
searchInput: '[data-testid="SearchBox_Search_Input"]',
confirmButton: '[data-testid="confirmationSheetConfirm"]',
};
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🚀 GROWTH AUTOMATION SUITE ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Storage helper
const storage = {
get: (key) => {
try {
return JSON.parse(localStorage.getItem(`xactions_growth_${key}`) || 'null');
} catch { return null; }
},
set: (key, value) => {
localStorage.setItem(`xactions_growth_${key}`, JSON.stringify(value));
}
};
// State
const state = {
follows: 0,
likes: 0,
unfollows: 0,
startTime: Date.now(),
isRunning: true,
};
// Tracked data
const followed = new Map(Object.entries(storage.get('followed') || {}));
const liked = new Set(storage.get('liked') || []);
const saveTracked = () => {
storage.set('followed', Object.fromEntries(followed));
storage.set('liked', Array.from(liked));
};
// Session check
const isSessionExpired = () => {
const elapsed = (Date.now() - state.startTime) / 1000 / 60;
return elapsed >= CONFIG.timing.sessionDuration;
};
const checkLimits = () => ({
canFollow: state.follows < CONFIG.limits.follows,
canLike: state.likes < CONFIG.limits.likes,
canUnfollow: state.unfollows < CONFIG.limits.unfollows,
});
// Log helper
const log = (msg, type = 'info') => {
const emoji = { info: '📘', success: '✅', warning: '⚠️', error: '❌' }[type] || '📘';
console.log(`${emoji} ${msg}`);
};
// Follow action
const doFollow = async (userCell) => {
if (!CONFIG.actions.follow || !checkLimits().canFollow) return false;
const followBtn = userCell.querySelector(SELECTORS.followButton);
if (!followBtn) return false;
const link = userCell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '').toLowerCase();
if (!username || followed.has(username)) return false;
followBtn.click();
state.follows++;
followed.set(username, { at: Date.now(), source: 'growth' });
saveTracked();
log(`Followed @${username} (${state.follows}/${CONFIG.limits.follows})`, 'success');
return true;
};
// Like action
const doLike = async (tweet) => {
if (!CONFIG.actions.like || !checkLimits().canLike) return false;
const likeBtn = tweet.querySelector(SELECTORS.likeButton);
if (!likeBtn) return false;
// The first /status/ link can belong to a quoted tweet; the anchor around
// the timestamp is the tweet's own permalink
const timeEl = tweet.querySelector('time');
const tweetLink = (timeEl && timeEl.closest('a[href*="/status/"]')) || tweet.querySelector('a[href*="/status/"]');
const tweetId = tweetLink?.href?.match(/status\/(\d+)/)?.[1];
if (!tweetId || liked.has(tweetId)) return false;
likeBtn.click();
state.likes++;
liked.add(tweetId);
saveTracked();
log(`Liked tweet (${state.likes}/${CONFIG.limits.likes})`, 'success');
return true;
};
// Unfollow action (for non-followers)
const doUnfollow = async (userCell) => {
if (!CONFIG.actions.unfollow || !checkLimits().canUnfollow) return false;
const unfollowBtn = userCell.querySelector(SELECTORS.unfollowButton);
if (!unfollowBtn) return false;
const link = userCell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '').toLowerCase();
if (!username) return false;
// Check if we followed this user and grace period has passed
const followData = followed.get(username);
if (followData) {
const daysSinceFollow = (Date.now() - followData.at) / 1000 / 60 / 60 / 24;
if (daysSinceFollow < CONFIG.timing.unfollowAfterDays) return false;
}
// Check if they follow back (look for "Follows you" indicator)
const followsYou = userCell.querySelector('[data-testid="userFollowIndicator"]');
if (followsYou) return false;
unfollowBtn.click();
await sleep(500);
const confirmBtn = document.querySelector(SELECTORS.confirmButton);
if (confirmBtn) confirmBtn.click();
state.unfollows++;
followed.delete(username);
saveTracked();
log(`Unfollowed @${username} (${state.unfollows}/${CONFIG.limits.unfollows})`, 'success');
return true;
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Growth = {
state,
config: CONFIG,
// Run auto-like on current feed
autoLike: async () => {
log('Starting auto-like on current feed...');
state.isRunning = true; // re-arm: stop() latches this false for every action
while (checkLimits().canLike && !isSessionExpired() && state.isRunning) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
for (const tweet of tweets) {
if (!state.isRunning || !checkLimits().canLike) break;
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent?.toLowerCase() || '';
const matchesKeyword = CONFIG.keywords.length === 0 ||
CONFIG.keywords.some(kw => text.includes(kw.toLowerCase()));
if (matchesKeyword) {
await doLike(tweet);
await randomDelay(CONFIG.timing.delayBetweenActions, CONFIG.timing.delayBetweenActions * 1.5);
}
}
window.scrollBy(0, window.innerHeight);
await sleep(2000);
}
log(`Auto-like complete. Liked ${state.likes} tweets.`);
},
// Run auto-follow on search results or user lists
autoFollow: async () => {
log('Starting auto-follow...');
log('📍 Navigate to a search results or followers list first');
state.isRunning = true; // re-arm: stop() latches this false for every action
while (checkLimits().canFollow && !isSessionExpired() && state.isRunning) {
const userCells = document.querySelectorAll(SELECTORS.userCell);
for (const cell of userCells) {
if (!state.isRunning || !checkLimits().canFollow) break;
await doFollow(cell);
await randomDelay(CONFIG.timing.delayBetweenActions, CONFIG.timing.delayBetweenActions * 1.5);
}
window.scrollBy(0, window.innerHeight);
await sleep(2000);
}
log(`Auto-follow complete. Followed ${state.follows} users.`);
},
// Run smart unfollow
smartUnfollow: async () => {
log('Starting smart unfollow...');
log('📍 Navigate to your Following list first');
state.isRunning = true; // re-arm: stop() latches this false for every action
while (checkLimits().canUnfollow && !isSessionExpired() && state.isRunning) {
const userCells = document.querySelectorAll(SELECTORS.userCell);
for (const cell of userCells) {
if (!state.isRunning || !checkLimits().canUnfollow) break;
await doUnfollow(cell);
await randomDelay(CONFIG.timing.delayBetweenActions * 1.5, CONFIG.timing.delayBetweenActions * 2);
}
window.scrollBy(0, window.innerHeight);
await sleep(2000);
}
log(`Smart unfollow complete. Unfollowed ${state.unfollows} users.`);
},
// Stop all automation
stop: () => {
state.isRunning = false;
log('Automation stopped.', 'warning');
},
// Get stats
stats: () => {
console.log('');
console.log('📊 GROWTH STATS:');
console.log(` 👥 Follows: ${state.follows}/${CONFIG.limits.follows}`);
console.log(` ❤️ Likes: ${state.likes}/${CONFIG.limits.likes}`);
console.log(` 🚫 Unfollows: ${state.unfollows}/${CONFIG.limits.unfollows}`);
console.log(` 📈 Total tracked follows: ${followed.size}`);
console.log('');
},
// Help
help: () => {
console.log('');
console.log('📋 GROWTH SUITE COMMANDS:');
console.log('');
console.log(' XActions.Growth.autoLike() - Auto-like feed posts');
console.log(' XActions.Growth.autoFollow() - Auto-follow users');
console.log(' XActions.Growth.smartUnfollow() - Unfollow non-followers');
console.log(' XActions.Growth.stop() - Stop automation');
console.log(' XActions.Growth.stats() - Show statistics');
console.log('');
},
};
log('Growth Suite loaded! Use XActions.Growth.help() for commands.');
console.log('');
})();
});
register("hashtag-analytics", function(){
var CONFIG = {
// Number of posts to analyze
maxPosts: 100,
// Delay between scrolls (ms)
scrollDelay: 1500,
// Maximum scroll attempts
maxScrolls: 50,
// Retry when no new posts found
maxRetries: 3
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function hashtagAnalytics() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ #️⃣ XActions — Hashtag Analytics ║
║ Track which hashtags drive the most engagement ║
╚══════════════════════════════════════════════════════════════╝
`);
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
const $like = '[data-testid="like"], [data-testid="unlike"]';
const $retweet = '[data-testid="retweet"], [data-testid="unretweet"]';
const $reply = '[data-testid="reply"]';
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '').trim();
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
const username = window.location.pathname.match(/^\/([^\/]+)/)?.[1];
if (!username || ['home', 'explore', 'notifications', 'messages', 'i'].includes(username)) {
console.error('❌ Please navigate to a profile page first!');
return;
}
console.log(`📊 Analyzing hashtags for @${username}\n`);
console.log('🔄 Scrolling to collect posts...\n');
const posts = new Map();
let retries = 0;
let scrollCount = 0;
while (posts.size < CONFIG.maxPosts && scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = posts.size;
document.querySelectorAll($tweet).forEach(tweet => {
const timeLink = tweet.querySelector('a[href*="/status/"] time')?.closest('a');
const tweetUrl = timeLink?.getAttribute('href') || '';
const tweetId = tweetUrl.split('/status/')[1]?.split('?')[0];
if (!tweetId || posts.has(tweetId)) return;
const textEl = tweet.querySelector($tweetText);
const text = textEl?.textContent || '';
// Extract hashtags
const hashtags = (text.match(/#\w+/g) || []).map(h => h.toLowerCase());
const likeBtn = tweet.querySelector($like);
const retweetBtn = tweet.querySelector($retweet);
const replyBtn = tweet.querySelector($reply);
const likes = parseCount(likeBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const retweets = parseCount(retweetBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
const replies = parseCount(replyBtn?.getAttribute('aria-label')?.match(/(\d[\d,.]*[KMB]?)/)?.[1] || '0');
posts.set(tweetId, {
id: tweetId,
text,
hashtags,
likes,
retweets,
replies,
engagement: likes + retweets + replies,
url: `https://x.com${tweetUrl}`
});
});
if (posts.size === prevSize) retries++;
else retries = 0;
console.log(` Found ${posts.size} posts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
const postsArray = Array.from(posts.values());
if (postsArray.length === 0) {
console.error('❌ No posts found.');
return;
}
// Analyze hashtags
const hashtagStats = {};
let postsWithHashtags = 0;
let postsWithoutHashtags = 0;
postsArray.forEach(p => {
if (p.hashtags.length > 0) {
postsWithHashtags++;
p.hashtags.forEach(tag => {
if (!hashtagStats[tag]) {
hashtagStats[tag] = {
tag,
count: 0,
totalEngagement: 0,
posts: []
};
}
hashtagStats[tag].count++;
hashtagStats[tag].totalEngagement += p.engagement;
hashtagStats[tag].posts.push(p);
});
} else {
postsWithoutHashtags++;
}
});
// Calculate averages
Object.values(hashtagStats).forEach(stat => {
stat.avgEngagement = stat.totalEngagement / stat.count;
});
// Sort by different criteria
const byCount = Object.values(hashtagStats).sort((a, b) => b.count - a.count);
const byEngagement = Object.values(hashtagStats).filter(h => h.count >= 2).sort((a, b) => b.avgEngagement - a.avgEngagement);
const byTotal = Object.values(hashtagStats).sort((a, b) => b.totalEngagement - a.totalEngagement);
const formatNum = (n) => {
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
return Math.round(n).toString();
};
console.log(`\n✅ Analyzed ${postsArray.length} posts\n`);
console.log('═'.repeat(60));
console.log('#️⃣ HASHTAG ANALYTICS');
console.log('═'.repeat(60));
console.log('\n📊 OVERVIEW:');
console.log('─'.repeat(50));
console.log(` Total posts analyzed: ${postsArray.length}`);
console.log(` Posts with hashtags: ${postsWithHashtags} (${((postsWithHashtags/postsArray.length)*100).toFixed(0)}%)`);
console.log(` Posts without hashtags: ${postsWithoutHashtags}`);
console.log(` Unique hashtags used: ${Object.keys(hashtagStats).length}`);
// Compare with/without hashtags
const withHashtagsAvg = postsArray.filter(p => p.hashtags.length > 0);
const withoutHashtagsAvg = postsArray.filter(p => p.hashtags.length === 0);
const avgWithHashtags = withHashtagsAvg.length > 0
? withHashtagsAvg.reduce((s, p) => s + p.engagement, 0) / withHashtagsAvg.length
: 0;
const avgWithoutHashtags = withoutHashtagsAvg.length > 0
? withoutHashtagsAvg.reduce((s, p) => s + p.engagement, 0) / withoutHashtagsAvg.length
: 0;
console.log('\n📈 HASHTAG IMPACT:');
console.log('─'.repeat(50));
console.log(` Avg engagement WITH hashtags: ${formatNum(avgWithHashtags)}`);
console.log(` Avg engagement WITHOUT hashtags: ${formatNum(avgWithoutHashtags)}`);
if (avgWithHashtags > avgWithoutHashtags && avgWithoutHashtags > 0) {
const improvement = ((avgWithHashtags - avgWithoutHashtags) / avgWithoutHashtags * 100).toFixed(0);
console.log(` 📈 Hashtags improve engagement by ${improvement}%`);
} else if (avgWithoutHashtags > avgWithHashtags) {
const decrease = ((avgWithoutHashtags - avgWithHashtags) / avgWithoutHashtags * 100).toFixed(0);
console.log(` 📉 Posts without hashtags perform ${decrease}% better`);
}
if (byCount.length > 0) {
console.log('\n🔢 MOST USED HASHTAGS:');
console.log('─'.repeat(50));
byCount.slice(0, 10).forEach((h, i) => {
const bar = '█'.repeat(Math.min(20, Math.round(h.count / byCount[0].count * 20)));
console.log(` ${(i + 1).toString().padStart(2)}. ${h.tag.padEnd(20)} ${bar} ${h.count}x`);
});
}
if (byEngagement.length > 0) {
console.log('\n🏆 HIGHEST PERFORMING HASHTAGS (min 2 uses):');
console.log('─'.repeat(50));
byEngagement.slice(0, 10).forEach((h, i) => {
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : ' ';
console.log(` ${medal} ${h.tag.padEnd(20)} ${formatNum(h.avgEngagement)} avg (${h.count} posts)`);
});
}
if (byTotal.length > 0) {
console.log('\n💰 HASHTAGS BY TOTAL ENGAGEMENT:');
console.log('─'.repeat(50));
byTotal.slice(0, 10).forEach((h, i) => {
console.log(` ${(i + 1).toString().padStart(2)}. ${h.tag.padEnd(20)} ${formatNum(h.totalEngagement)} total`);
});
}
// Hashtag combinations
console.log('\n🔗 HASHTAG COMBINATIONS:');
console.log('─'.repeat(50));
const combos = {};
postsArray.filter(p => p.hashtags.length >= 2).forEach(p => {
const sorted = [...p.hashtags].sort();
for (let i = 0; i < sorted.length; i++) {
for (let j = i + 1; j < sorted.length; j++) {
const key = `${sorted[i]} + ${sorted[j]}`;
if (!combos[key]) combos[key] = { count: 0, totalEngagement: 0 };
combos[key].count++;
combos[key].totalEngagement += p.engagement;
}
}
});
const topCombos = Object.entries(combos)
.filter(([_, data]) => data.count >= 2)
.map(([combo, data]) => ({ combo, ...data, avg: data.totalEngagement / data.count }))
.sort((a, b) => b.avg - a.avg)
.slice(0, 5);
if (topCombos.length > 0) {
topCombos.forEach((c, i) => {
console.log(` ${i + 1}. ${c.combo} — ${formatNum(c.avg)} avg (${c.count}x)`);
});
} else {
console.log(' Not enough data on hashtag combinations.');
}
// Recommendations
console.log('\n' + '═'.repeat(60));
console.log('💡 RECOMMENDATIONS');
console.log('═'.repeat(60));
if (byEngagement.length > 0) {
console.log(`\n✅ Best performing hashtags to keep using:`);
byEngagement.slice(0, 5).forEach(h => {
console.log(` ${h.tag}`);
});
}
const lowPerformers = Object.values(hashtagStats)
.filter(h => h.count >= 3 && h.avgEngagement < avgWithHashtags * 0.5)
.sort((a, b) => a.avgEngagement - b.avgEngagement);
if (lowPerformers.length > 0) {
console.log(`\n❌ Consider dropping these underperforming hashtags:`);
lowPerformers.slice(0, 5).forEach(h => {
console.log(` ${h.tag} (${formatNum(h.avgEngagement)} avg vs ${formatNum(avgWithHashtags)} overall)`);
});
}
// Save analysis
const storageKey = `xactions_hashtags_${username}`;
localStorage.setItem(storageKey, JSON.stringify({
username,
timestamp: new Date().toISOString(),
postCount: postsArray.length,
postsWithHashtags,
uniqueHashtags: Object.keys(hashtagStats).length,
avgWithHashtags,
avgWithoutHashtags,
topByEngagement: byEngagement.slice(0, 20).map(h => ({ tag: h.tag, count: h.count, avg: h.avgEngagement })),
topByCount: byCount.slice(0, 20).map(h => ({ tag: h.tag, count: h.count }))
}));
console.log('\n' + '═'.repeat(60));
console.log(`💾 Analysis saved! Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('═'.repeat(60) + '\n');
})();
});
register("interact-by-hashtag", function(){
var CONFIG = {
// Hashtags to target (without #)
hashtags: [
'crypto',
'web3',
'bitcoin',
],
// Actions to perform
actions: {
like: true,
retweet: false,
follow: true,
},
// Limits per session
limits: {
likes: 20,
retweets: 5,
follows: 10,
tweetsPerHashtag: 10,
},
// Filters
filters: {
minLikes: 5, // Minimum likes on tweet
minRetweets: 0, // Minimum retweets
skipReplies: true, // Skip reply tweets
skipRetweets: true, // Skip retweets
requireMedia: false, // Only posts with images/video
},
// Timing
delayBetweenActions: 2000,
scrollDelay: 2000,
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function interactByHashtag() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = (min, max) => sleep(Math.random() * (max - min) + min);
// DOM Selectors
const SELECTORS = {
tweet: '[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
retweetButton: '[data-testid="retweet"]',
followButton: '[data-testid$="-follow"]',
searchInput: '[data-testid="SearchBox_Search_Input"]',
userCell: '[data-testid="UserCell"]',
};
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ #️⃣ INTERACT BY HASHTAG ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// State
const state = {
isRunning: false,
currentHashtag: null,
stats: { likes: 0, retweets: 0, follows: 0 },
processedTweets: new Set(),
};
// Parse counts like "1,234" / "5.2K" from button aria-labels
const parseCount = (str) => {
if (!str) return 0;
const match = str.replace(/,/g, '').match(/([\d.]+)([KMB])?/i);
if (!match) return 0;
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num) || 0;
};
// Helper to check if tweet passes filters
const passesFilters = (tweet) => {
// Check if already liked
if (tweet.querySelector(SELECTORS.unlikeButton)) return false;
// Check for replies (structural marker first; text only works on English UIs)
if (CONFIG.filters.skipReplies) {
const isReply = tweet.querySelector('[data-testid="in-reply-to"]') !== null ||
Array.from(tweet.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
if (isReply) return false;
}
// Check for retweets: socialContext inside an <a> = repost (locale-independent);
// a plain socialContext is a pinned post
if (CONFIG.filters.skipRetweets) {
const socialContext = tweet.querySelector('[data-testid="socialContext"]');
if (socialContext && socialContext.closest('a')) return false;
}
// Check for media
if (CONFIG.filters.requireMedia) {
const hasMedia = tweet.querySelector('[data-testid="tweetPhoto"]') ||
tweet.querySelector('[data-testid="videoPlayer"], [data-testid="videoComponent"]');
if (!hasMedia) return false;
}
// Check engagement minimums
if (CONFIG.filters.minLikes > 0) {
const likeEl = tweet.querySelector(`${SELECTORS.likeButton}, ${SELECTORS.unlikeButton}`);
if (parseCount(likeEl?.getAttribute('aria-label')) < CONFIG.filters.minLikes) return false;
}
if (CONFIG.filters.minRetweets > 0) {
const rtEl = tweet.querySelector(`${SELECTORS.retweetButton}, [data-testid="unretweet"]`);
if (parseCount(rtEl?.getAttribute('aria-label')) < CONFIG.filters.minRetweets) return false;
}
return true;
};
// Get tweet ID: the anchor around the timestamp is the tweet's own permalink;
// the first /status/ link can belong to a quoted tweet
const getTweetId = (tweet) => {
const timeEl = tweet.querySelector('time');
const link = (timeEl && timeEl.closest('a[href*="/status/"]')) || tweet.querySelector('a[href*="/status/"]');
return link?.href?.match(/status\/(\d+)/)?.[1];
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Hashtag = {
config: CONFIG,
state,
// Search for a specific hashtag
search: async (hashtag) => {
const tag = hashtag?.replace('#', '') || CONFIG.hashtags[0];
if (!tag) {
console.error('❌ No hashtag specified!');
return;
}
console.log(`🔍 Searching for #${tag}...`);
// Type into X's own search box and submit through it (a real in-app
// search) instead of assigning location.href. Setting location.href
// forces a hard page reload, which wipes this injected script (window.XActions
// included) - so the "run search(), then run interact()" flow documented
// above would break as soon as search() ran.
const input = document.querySelector(SELECTORS.searchInput);
if (input) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(input, `#${tag}`);
input.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(300);
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
console.log('✅ Search submitted. Once results load, run XActions.Hashtag.interact().');
} else {
console.warn('⚠️ Search box not found on this page. Falling back to a full navigation - this reloads the page and clears the script, so paste it again once results load.');
window.location.href = `https://x.com/search?q=%23${tag}&src=typed_query&f=live`;
}
},
// Interact with current search results
interact: async () => {
console.log('🚀 Starting hashtag interaction...');
state.isRunning = true;
let processed = 0;
let stalledScrolls = 0;
let lastProcessedCount = state.processedTweets.size;
let warnedAboutVolume = false;
while (state.isRunning && state.stats.likes < CONFIG.limits.likes) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
for (const tweet of tweets) {
if (!state.isRunning) break;
if (state.stats.likes >= CONFIG.limits.likes) break;
const tweetId = getTweetId(tweet);
if (!tweetId || state.processedTweets.has(tweetId)) continue;
state.processedTweets.add(tweetId);
if (!passesFilters(tweet)) continue;
// Like
if (CONFIG.actions.like && state.stats.likes < CONFIG.limits.likes) {
const likeBtn = tweet.querySelector(SELECTORS.likeButton);
if (likeBtn) {
likeBtn.click();
state.stats.likes++;
console.log(`❤️ Liked tweet (${state.stats.likes}/${CONFIG.limits.likes})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
// Retweet
if (CONFIG.actions.retweet && state.stats.retweets < CONFIG.limits.retweets) {
const rtBtn = tweet.querySelector(SELECTORS.retweetButton);
if (rtBtn) {
rtBtn.click();
await sleep(500);
const confirmBtn = document.querySelector('[data-testid="retweetConfirm"]');
if (confirmBtn) confirmBtn.click();
state.stats.retweets++;
console.log(`🔄 Retweeted (${state.stats.retweets}/${CONFIG.limits.retweets})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
// Follow user
if (CONFIG.actions.follow && state.stats.follows < CONFIG.limits.follows) {
const followBtn = tweet.querySelector(SELECTORS.followButton);
if (followBtn) {
followBtn.click();
state.stats.follows++;
console.log(`👥 Followed user (${state.stats.follows}/${CONFIG.limits.follows})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
processed++;
}
// Scroll for more
window.scrollBy(0, window.innerHeight);
await sleep(CONFIG.scrollDelay);
// End-of-results detection so the loop cannot scroll forever
if (state.processedTweets.size === lastProcessedCount) {
stalledScrolls++;
if (stalledScrolls >= 10) {
console.log('⚠️ No new tweets after 10 scrolls. Stopping.');
break;
}
} else {
stalledScrolls = 0;
lastProcessedCount = state.processedTweets.size;
}
if (processed > 50 && !warnedAboutVolume) {
warnedAboutVolume = true;
console.log('⚠️ Processed many tweets. Consider stopping to avoid rate limits.');
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 HASHTAG INTERACTION COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
window.XActions.Hashtag.stats();
},
// Search and interact with all configured hashtags
interactAll: async () => {
if (CONFIG.hashtags.length === 0) {
console.error('❌ No hashtags configured!');
return;
}
console.log(`🚀 Processing ${CONFIG.hashtags.length} hashtags...`);
console.log('');
console.log('📋 Run these commands in sequence:');
CONFIG.hashtags.forEach((tag, i) => {
console.log(` ${i + 1}. XActions.Hashtag.search("${tag}")`);
console.log(` Then: XActions.Hashtag.interact()`);
});
console.log('');
},
// Add hashtag
addHashtag: (tag) => {
const clean = tag.replace('#', '').toLowerCase();
if (!CONFIG.hashtags.includes(clean)) {
CONFIG.hashtags.push(clean);
console.log(`✅ Added #${clean}`);
}
},
// Remove hashtag
removeHashtag: (tag) => {
const clean = tag.replace('#', '').toLowerCase();
const idx = CONFIG.hashtags.indexOf(clean);
if (idx > -1) {
CONFIG.hashtags.splice(idx, 1);
console.log(`✅ Removed #${clean}`);
}
},
// Stop
stop: () => {
state.isRunning = false;
console.log('🛑 Stopped.');
},
// Stats
stats: () => {
console.log('');
console.log('📊 HASHTAG INTERACTION STATS:');
console.log(` ❤️ Likes: ${state.stats.likes}/${CONFIG.limits.likes}`);
console.log(` 🔄 Retweets: ${state.stats.retweets}/${CONFIG.limits.retweets}`);
console.log(` 👥 Follows: ${state.stats.follows}/${CONFIG.limits.follows}`);
console.log(` 📝 Tweets processed: ${state.processedTweets.size}`);
console.log('');
},
// Help
help: () => {
console.log('');
console.log('📋 HASHTAG INTERACTION COMMANDS:');
console.log('');
console.log(' XActions.Hashtag.search("crypto") - Search hashtag');
console.log(' XActions.Hashtag.interact() - Interact with results');
console.log(' XActions.Hashtag.addHashtag("tag") - Add hashtag');
console.log(' XActions.Hashtag.removeHashtag("t") - Remove hashtag');
console.log(' XActions.Hashtag.interactAll() - Show guide for all');
console.log(' XActions.Hashtag.stop() - Stop interaction');
console.log(' XActions.Hashtag.stats() - Show statistics');
console.log('');
}
};
console.log('✅ Interact By Hashtag loaded!');
console.log(`📋 Configured hashtags: ${CONFIG.hashtags.map(t => '#' + t).join(', ')}`);
console.log(' Run XActions.Hashtag.help() for commands.');
console.log('');
})();
});
register("interact-by-place", function(){
var CONFIG = {
// Target locations
locations: [
{ name: 'New York', query: 'near:"New York"' },
{ name: 'San Francisco', query: 'near:"San Francisco"' },
// { name: 'London', query: 'near:"London"' },
],
// Keywords to combine with location
keywords: [
// 'coffee',
// 'startup',
// 'tech',
],
// Actions
actions: {
like: true,
follow: true,
retweet: false,
},
// Limits
limits: {
likes: 15,
follows: 10,
retweets: 3,
},
// Timing
delayBetweenActions: 2000,
scrollDelay: 2000,
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function interactByPlace() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = (min, max) => sleep(Math.random() * (max - min) + min);
// DOM Selectors
const SELECTORS = {
tweet: '[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
retweetButton: '[data-testid="retweet"]',
followButton: '[data-testid$="-follow"]',
searchInput: '[data-testid="SearchBox_Search_Input"]',
};
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📍 INTERACT BY PLACE ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// State
const state = {
isRunning: false,
currentLocation: null,
stats: { likes: 0, follows: 0, retweets: 0 },
processedTweets: new Set(),
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Place = {
config: CONFIG,
state,
// Search by location
search: async (locationName, keyword = '') => {
let location = CONFIG.locations.find(l =>
l.name.toLowerCase() === locationName?.toLowerCase()
);
if (!location && locationName) {
// Use custom location
location = { name: locationName, query: `near:"${locationName}"` };
}
if (!location) {
location = CONFIG.locations[0];
}
if (!location) {
console.error('❌ No location specified or configured!');
return;
}
let searchQuery = location.query;
if (keyword) {
searchQuery = `${keyword} ${location.query}`;
}
console.log(`📍 Searching: ${searchQuery}`);
// Type into X's own search box and submit through it (a real in-app
// search) instead of assigning location.href. Setting location.href
// forces a hard page reload, which wipes this injected script (window.XActions
// included) before interact() could ever be called on the results.
const input = document.querySelector(SELECTORS.searchInput);
if (input) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(input, searchQuery);
input.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(300);
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true }));
console.log('✅ Search submitted. Once results load, run XActions.Place.interact().');
} else {
console.warn('⚠️ Search box not found on this page. Falling back to a full navigation - this reloads the page and clears the script, so paste it again once results load.');
const encodedQuery = encodeURIComponent(searchQuery);
window.location.href = `https://x.com/search?q=${encodedQuery}&src=typed_query&f=live`;
}
},
// Interact with search results
interact: async () => {
console.log('🚀 Starting location-based interaction...');
state.isRunning = true;
let stalledScrolls = 0;
let lastProcessedCount = state.processedTweets.size;
while (state.isRunning && state.stats.likes < CONFIG.limits.likes) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
for (const tweet of tweets) {
if (!state.isRunning) break;
if (state.stats.likes >= CONFIG.limits.likes) break;
// The anchor around the timestamp is the tweet's own permalink; the
// first /status/ link can belong to a quoted tweet
const timeEl = tweet.querySelector('time');
const tweetLink = (timeEl && timeEl.closest('a[href*="/status/"]')) || tweet.querySelector('a[href*="/status/"]');
const tweetId = tweetLink?.href?.match(/status\/(\d+)/)?.[1];
if (!tweetId || state.processedTweets.has(tweetId)) continue;
state.processedTweets.add(tweetId);
// Skip already liked
if (tweet.querySelector(SELECTORS.unlikeButton)) continue;
// Like
if (CONFIG.actions.like && state.stats.likes < CONFIG.limits.likes) {
const likeBtn = tweet.querySelector(SELECTORS.likeButton);
if (likeBtn) {
likeBtn.click();
state.stats.likes++;
console.log(`❤️ Liked tweet (${state.stats.likes}/${CONFIG.limits.likes})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
// Follow
if (CONFIG.actions.follow && state.stats.follows < CONFIG.limits.follows) {
const followBtn = tweet.querySelector(SELECTORS.followButton);
if (followBtn) {
followBtn.click();
state.stats.follows++;
console.log(`👥 Followed (${state.stats.follows}/${CONFIG.limits.follows})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
// Retweet
if (CONFIG.actions.retweet && state.stats.retweets < CONFIG.limits.retweets) {
const rtBtn = tweet.querySelector(SELECTORS.retweetButton);
if (rtBtn) {
rtBtn.click();
await sleep(500);
const confirmBtn = document.querySelector('[data-testid="retweetConfirm"]');
if (confirmBtn) confirmBtn.click();
state.stats.retweets++;
console.log(`🔄 Retweeted (${state.stats.retweets}/${CONFIG.limits.retweets})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
}
window.scrollBy(0, window.innerHeight);
await sleep(CONFIG.scrollDelay);
// End-of-results detection so the loop cannot scroll forever
if (state.processedTweets.size === lastProcessedCount) {
stalledScrolls++;
if (stalledScrolls >= 10) {
console.log('⚠️ No new tweets after 10 scrolls. Stopping.');
break;
}
} else {
stalledScrolls = 0;
lastProcessedCount = state.processedTweets.size;
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 LOCATION INTERACTION COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
window.XActions.Place.stats();
},
// Add location
addLocation: (name, customQuery = null) => {
const query = customQuery || `near:"${name}"`;
CONFIG.locations.push({ name, query });
console.log(`✅ Added location: ${name}`);
},
// List locations
listLocations: () => {
console.log('');
console.log('📍 CONFIGURED LOCATIONS:');
CONFIG.locations.forEach((l, i) => {
console.log(` ${i + 1}. ${l.name}: ${l.query}`);
});
console.log('');
},
// Stop
stop: () => {
state.isRunning = false;
console.log('🛑 Stopped.');
},
// Stats
stats: () => {
console.log('');
console.log('📊 LOCATION INTERACTION STATS:');
console.log(` ❤️ Likes: ${state.stats.likes}/${CONFIG.limits.likes}`);
console.log(` 👥 Follows: ${state.stats.follows}/${CONFIG.limits.follows}`);
console.log(` 🔄 Retweets: ${state.stats.retweets}/${CONFIG.limits.retweets}`);
console.log('');
},
// Help
help: () => {
console.log('');
console.log('📋 PLACE INTERACTION COMMANDS:');
console.log('');
console.log(' XActions.Place.search("New York")');
console.log(' XActions.Place.search("NYC", "coffee")');
console.log(' XActions.Place.interact()');
console.log(' XActions.Place.addLocation("Miami")');
console.log(' XActions.Place.listLocations()');
console.log(' XActions.Place.stop()');
console.log(' XActions.Place.stats()');
console.log('');
}
};
console.log('✅ Interact By Place loaded!');
console.log(`📍 Configured locations: ${CONFIG.locations.length}`);
console.log(' Run XActions.Place.help() for commands.');
console.log('');
})();
});
register("interact-by-users", function(){
var CONFIG = {
// Target usernames to interact with
targetUsers: [
// 'nichxbt',
// 'elonmusk',
],
// Actions to perform
actions: {
like: true,
retweet: false,
reply: false,
follow: true,
},
// Limits per user
limits: {
likesPerUser: 3,
retweetsPerUser: 1,
repliesPerUser: 1,
},
// Timing
delayBetweenActions: 2000,
delayBetweenUsers: 5000,
// Reply templates (random selection)
replyTemplates: [
'Great point! 🔥',
'Couldn\'t agree more 👏',
'This is gold 💯',
'Thanks for sharing!',
],
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function interactByUsers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = (min, max) => sleep(Math.random() * (max - min) + min);
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
// DOM Selectors
const SELECTORS = {
tweet: '[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
retweetButton: '[data-testid="retweet"]',
unretweetButton: '[data-testid="unretweet"]',
replyButton: '[data-testid="reply"]',
followButton: '[data-testid$="-follow"]',
unfollowButton: '[data-testid$="-unfollow"]',
tweetInput: '[data-testid="tweetTextarea_0"]',
tweetSubmit: '[data-testid="tweetButton"]',
};
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 👤 INTERACT BY USERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Storage
const storage = {
get: (key) => {
try { return JSON.parse(localStorage.getItem(`xactions_interact_${key}`) || 'null'); }
catch { return null; }
},
set: (key, value) => {
localStorage.setItem(`xactions_interact_${key}`, JSON.stringify(value));
}
};
// Track interactions
const history = storage.get('history') || {};
const saveHistory = () => storage.set('history', history);
const getInteractionCount = (username, type) => {
if (!history[username]) history[username] = { likes: 0, retweets: 0, replies: 0, followed: false };
return history[username][type] || 0;
};
const recordInteraction = (username, type) => {
if (!history[username]) history[username] = { likes: 0, retweets: 0, replies: 0, followed: false };
if (type === 'followed') {
history[username].followed = true;
} else {
history[username][type] = (history[username][type] || 0) + 1;
}
saveHistory();
};
// State
const state = {
isRunning: false,
currentUser: null,
stats: { likes: 0, retweets: 0, replies: 0, follows: 0 },
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.InteractUsers = {
config: CONFIG,
state,
history,
// Interact with a single user's profile (must already be open, a console
// script does not survive a page navigation)
interactWith: async (username) => {
const cleanUsername = username.replace('@', '').toLowerCase();
const currentPath = window.location.pathname.toLowerCase().replace(/^\//, '').split('/')[0];
if (currentPath !== cleanUsername) {
console.error(`❌ You are not on @${cleanUsername}'s profile.`);
console.log(`📍 Navigate to https://x.com/${cleanUsername} first, then run this again.`);
return;
}
console.log(`👤 Starting interaction with @${cleanUsername}...`);
state.currentUser = cleanUsername;
state.isRunning = true;
// Wait for tweets to load
await sleep(2000);
const tweets = document.querySelectorAll(SELECTORS.tweet);
console.log(`🔍 Found ${tweets.length} tweets`);
let userLikes = 0;
let userRetweets = 0;
let userReplies = 0;
for (const tweet of tweets) {
if (!state.isRunning) break;
// Like
if (CONFIG.actions.like && userLikes < CONFIG.limits.likesPerUser) {
const likeBtn = tweet.querySelector(SELECTORS.likeButton);
if (likeBtn) {
likeBtn.click();
userLikes++;
state.stats.likes++;
recordInteraction(cleanUsername, 'likes');
console.log(`❤️ Liked tweet ${userLikes}/${CONFIG.limits.likesPerUser}`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
// Retweet
if (CONFIG.actions.retweet && userRetweets < CONFIG.limits.retweetsPerUser) {
const rtBtn = tweet.querySelector(SELECTORS.retweetButton);
if (rtBtn) {
rtBtn.click();
await sleep(500);
// Confirm retweet
const confirmBtn = document.querySelector('[data-testid="retweetConfirm"]');
if (confirmBtn) confirmBtn.click();
userRetweets++;
state.stats.retweets++;
recordInteraction(cleanUsername, 'retweets');
console.log(`🔄 Retweeted ${userRetweets}/${CONFIG.limits.retweetsPerUser}`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
// Reply (was declared in CONFIG/SELECTORS but never wired up)
if (CONFIG.actions.reply && userReplies < CONFIG.limits.repliesPerUser) {
const replyBtn = tweet.querySelector(SELECTORS.replyButton);
if (replyBtn) {
replyBtn.click();
await sleep(1000);
const input = document.querySelector(SELECTORS.tweetInput);
const submitBtn = document.querySelector(SELECTORS.tweetSubmit);
if (input && submitBtn) {
input.focus();
document.execCommand('insertText', false, randomItem(CONFIG.replyTemplates));
await sleep(300);
if (!submitBtn.disabled) {
submitBtn.click();
userReplies++;
state.stats.replies++;
recordInteraction(cleanUsername, 'replies');
console.log(`💬 Replied ${userReplies}/${CONFIG.limits.repliesPerUser}`);
await sleep(800);
} else {
console.warn('⚠️ Reply button stayed disabled; closing composer.');
document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
}
} else {
console.warn('⚠️ Reply composer did not open as expected.');
}
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
}
}
}
// Follow
if (CONFIG.actions.follow && !history[cleanUsername]?.followed) {
const followBtn = document.querySelector(SELECTORS.followButton);
if (followBtn) {
followBtn.click();
state.stats.follows++;
recordInteraction(cleanUsername, 'followed');
console.log(`👥 Followed @${cleanUsername}`);
}
}
console.log(`✅ Completed interaction with @${cleanUsername}`);
console.log(` Likes: ${userLikes}, Retweets: ${userRetweets}, Replies: ${userReplies}`);
state.currentUser = null;
},
// Interact with all target users: a console script cannot navigate between
// profiles without being wiped, so this walks you through the sequence
interactAll: async () => {
if (CONFIG.targetUsers.length === 0) {
console.error('❌ No target users configured!');
console.log('Add usernames to CONFIG.targetUsers array.');
return;
}
console.log(`🚀 Processing ${CONFIG.targetUsers.length} users...`);
console.log('');
console.log('📋 For each user, open their profile and run:');
CONFIG.targetUsers.forEach((username, i) => {
console.log(` ${i + 1}. https://x.com/${username}`);
console.log(` Then: XActions.InteractUsers.interactWith("${username}")`);
});
console.log('');
console.log('💡 Re-paste this script after each page navigation.');
},
// Add user to target list
addUser: (username) => {
const clean = username.replace('@', '').toLowerCase();
if (!CONFIG.targetUsers.includes(clean)) {
CONFIG.targetUsers.push(clean);
console.log(`✅ Added @${clean} to target list`);
} else {
console.log(`⚠️ @${clean} already in target list`);
}
},
// Remove user from target list
removeUser: (username) => {
const clean = username.replace('@', '').toLowerCase();
const idx = CONFIG.targetUsers.indexOf(clean);
if (idx > -1) {
CONFIG.targetUsers.splice(idx, 1);
console.log(`✅ Removed @${clean} from target list`);
}
},
// Stop interaction
stop: () => {
state.isRunning = false;
console.log('🛑 Interaction stopped.');
},
// Show stats
stats: () => {
console.log('');
console.log('📊 INTERACTION STATS:');
console.log(` ❤️ Total likes: ${state.stats.likes}`);
console.log(` 🔄 Total retweets: ${state.stats.retweets}`);
console.log(` 💬 Total replies: ${state.stats.replies}`);
console.log(` 👥 Total follows: ${state.stats.follows}`);
console.log('');
},
// Show history
showHistory: () => {
console.log('');
console.log('📜 INTERACTION HISTORY:');
Object.entries(history).forEach(([user, data]) => {
console.log(` @${user}: ${data.likes}L / ${data.retweets}RT / ${data.replies}R / ${data.followed ? '✓Following' : ''}`);
});
console.log('');
},
// Help
help: () => {
console.log('');
console.log('📋 INTERACT BY USERS COMMANDS:');
console.log('');
console.log(' XActions.InteractUsers.addUser("username")');
console.log(' XActions.InteractUsers.removeUser("username")');
console.log(' XActions.InteractUsers.interactWith("username")');
console.log(' XActions.InteractUsers.interactAll()');
console.log(' XActions.InteractUsers.stop()');
console.log(' XActions.InteractUsers.stats()');
console.log(' XActions.InteractUsers.showHistory()');
console.log('');
}
};
console.log('✅ Interact By Users loaded!');
console.log(`📋 Target users: ${CONFIG.targetUsers.length}`);
console.log(' Run XActions.InteractUsers.help() for commands.');
console.log('');
})();
});
register("interact-with-likers", function(){
var CONFIG = {
// Actions
actions: {
follow: true,
},
// Limits
limits: {
follows: 20,
},
// Filters
filters: {
skipPrivate: true,
skipVerified: false, // Skip verified accounts
skipNoPhoto: false, // Skip accounts with default photo
},
// Timing
delayBetweenActions: 2000,
scrollDelay: 2000,
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function interactWithLikers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = (min, max) => sleep(Math.random() * (max - min) + min);
// DOM Selectors
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
followButton: '[data-testid$="-follow"]',
unfollowButton: '[data-testid$="-unfollow"]',
verifiedBadge: '[data-testid="icon-verified"]',
userName: '[data-testid="User-Name"]',
protectedIcon: '[data-testid="icon-lock"]',
defaultAvatar: 'img[src*="default_profile"]',
};
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ❤️ INTERACT WITH LIKERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on a likes page
if (!window.location.href.includes('/likes')) {
console.error('❌ ERROR: You must be on a tweet\'s Likes page!');
console.log('');
console.log('📋 How to get there:');
console.log(' 1. Go to any tweet');
console.log(' 2. Click on "X likes" below the tweet');
console.log(' 3. URL should be: x.com/USER/status/ID/likes');
console.log('');
return;
}
// State
const state = {
isRunning: false,
stats: { followed: 0, skipped: 0 },
processedUsers: new Set(),
};
// Check if user passes filters
const passesFilters = (userCell) => {
// Skip if already following
if (userCell.querySelector(SELECTORS.unfollowButton)) return false;
// Skip verified if configured
if (CONFIG.filters.skipVerified && userCell.querySelector(SELECTORS.verifiedBadge)) {
return false;
}
// Skip protected/private accounts if configured (was declared but never checked)
if (CONFIG.filters.skipPrivate && userCell.querySelector(SELECTORS.protectedIcon)) {
return false;
}
// Skip accounts still on the default avatar if configured (was declared but never checked)
if (CONFIG.filters.skipNoPhoto && userCell.querySelector(SELECTORS.defaultAvatar)) {
return false;
}
return true;
};
// Get username from cell
const getUsername = (userCell) => {
const link = userCell.querySelector('a[href^="/"]');
return link?.getAttribute('href')?.replace('/', '');
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Likers = {
config: CONFIG,
state,
// Follow likers
follow: async () => {
console.log('🚀 Starting to follow likers...');
state.isRunning = true;
state.stats = { followed: 0, skipped: 0 };
let emptyScrolls = 0;
while (state.isRunning && state.stats.followed < CONFIG.limits.follows) {
const userCells = document.querySelectorAll(SELECTORS.userCell);
for (const cell of userCells) {
if (!state.isRunning) break;
if (state.stats.followed >= CONFIG.limits.follows) break;
const username = getUsername(cell);
if (!username || state.processedUsers.has(username)) continue;
state.processedUsers.add(username);
if (!passesFilters(cell)) {
state.stats.skipped++;
continue;
}
const followBtn = cell.querySelector(SELECTORS.followButton);
if (followBtn) {
try {
followBtn.click();
state.stats.followed++;
console.log(`👥 Followed @${username} (${state.stats.followed}/${CONFIG.limits.follows})`);
await randomDelay(CONFIG.delayBetweenActions, CONFIG.delayBetweenActions * 1.5);
} catch (e) {
console.warn(`⚠️ Failed to follow @${username}`);
}
}
}
// Scroll for more
window.scrollBy(0, window.innerHeight);
await sleep(CONFIG.scrollDelay);
// Check if we've reached the end
const newCells = document.querySelectorAll(SELECTORS.userCell);
const allProcessed = [...newCells].every(cell => {
const username = getUsername(cell);
return state.processedUsers.has(username);
});
if (allProcessed && newCells.length > 0) {
console.log('📄 Reached end of likers list.');
break;
}
// Nothing rendered at all: stop after a few tries instead of looping forever
if (newCells.length === 0) {
emptyScrolls++;
if (emptyScrolls >= 5) {
console.log('⚠️ No likers found after 5 scrolls. Stopping.');
break;
}
} else {
emptyScrolls = 0;
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 FINISHED FOLLOWING LIKERS! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
window.XActions.Likers.stats();
},
// Collect likers (just get usernames)
collect: async () => {
console.log('📥 Collecting likers...');
const likers = [];
let scrollCount = 0;
const maxScrolls = 20;
while (scrollCount < maxScrolls) {
const userCells = document.querySelectorAll(SELECTORS.userCell);
userCells.forEach(cell => {
const username = getUsername(cell);
if (username && !likers.includes(username)) {
likers.push(username);
}
});
console.log(` 📊 Collected ${likers.length} likers...`);
window.scrollBy(0, window.innerHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log('');
console.log(`✅ Collected ${likers.length} likers!`);
console.log('📋 Likers:', likers.join(', '));
return likers;
},
// Stop
stop: () => {
state.isRunning = false;
console.log('🛑 Stopped.');
},
// Stats
stats: () => {
console.log('');
console.log('📊 LIKERS INTERACTION STATS:');
console.log(` 👥 Followed: ${state.stats.followed}`);
console.log(` ⏭️ Skipped: ${state.stats.skipped}`);
console.log(` 📝 Total processed: ${state.processedUsers.size}`);
console.log('');
},
// Help
help: () => {
console.log('');
console.log('📋 LIKERS INTERACTION COMMANDS:');
console.log('');
console.log(' XActions.Likers.follow() - Follow likers');
console.log(' XActions.Likers.collect() - Just collect usernames');
console.log(' XActions.Likers.stop() - Stop following');
console.log(' XActions.Likers.stats() - Show statistics');
console.log('');
console.log('📍 Make sure you\'re on a likes page first!');
console.log(' URL: x.com/USER/status/ID/likes');
console.log('');
}
};
console.log('✅ Interact With Likers loaded!');
console.log(' Run XActions.Likers.follow() to start following.');
console.log(' Run XActions.Likers.help() for all commands.');
console.log('');
})();
});
register("join-communities", function(){
var CONFIG = {
// Community IDs to join (get from URL: x.com/i/communities/1234567890)
communities: [
// '1234567890123456789',
// '9876543210987654321',
],
// Delay between joining communities (ms)
joinDelay: 3000,
// Delay for page load (ms)
navigationDelay: 3000,
// Maximum communities to join (0 = all in list)
maxJoin: 0,
// Skip communities you've already joined
skipAlreadyJoined: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function joinCommunities() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $joinButton = 'button[aria-label^="Join"]:not([aria-label^="Joined"])';
const $joinedButton = 'button[aria-label^="Joined"]';
const $pendingButton = 'button[aria-label^="Pending"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🏘️ JOIN COMMUNITIES FROM LIST ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Validate configuration
if (!CONFIG.communities || CONFIG.communities.length === 0) {
console.error('❌ ERROR: No communities configured!');
console.log('');
console.log('📋 How to configure:');
console.log(' 1. Find community IDs from URLs: x.com/i/communities/1234567890');
console.log(' 2. Add them to CONFIG.communities array:');
console.log(' communities: [');
console.log(' "1234567890123456789",');
console.log(' "9876543210987654321",');
console.log(' ]');
return;
}
// State tracking (persists across page navigations)
const getJoinedCommunities = () => {
try {
return JSON.parse(sessionStorage.getItem('xactions_joined_communities') || '[]');
} catch { return []; }
};
const markAsJoined = (id, status) => {
const joined = getJoinedCommunities();
if (!joined.find(c => c.id === id)) {
joined.push({ id, status, timestamp: Date.now() });
sessionStorage.setItem('xactions_joined_communities', JSON.stringify(joined));
}
};
const getCurrentIndex = () => {
return parseInt(sessionStorage.getItem('xactions_join_index') || '0', 10);
};
const setCurrentIndex = (idx) => {
sessionStorage.setItem('xactions_join_index', idx.toString());
};
console.log(`📋 Communities to join: ${CONFIG.communities.length}`);
console.log(`✅ Already processed: ${getJoinedCommunities().length}`);
console.log('');
const processCurrentCommunity = async () => {
const index = getCurrentIndex();
// Check if we're done
if (index >= CONFIG.communities.length) {
const joined = getJoinedCommunities();
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(`║ 🎉 COMPLETE! Processed ${joined.length} communities `);
console.log('╚════════════════════════════════════════════════════════════╝');
// Summary
const successful = joined.filter(c => c.status === 'joined').length;
const pending = joined.filter(c => c.status === 'pending').length;
const skipped = joined.filter(c => c.status === 'already_joined').length;
const failed = joined.filter(c => c.status === 'failed').length;
console.log(` ✅ Joined: ${successful}`);
console.log(` ⏳ Pending approval: ${pending}`);
console.log(` ⏭️ Already joined: ${skipped}`);
console.log(` ❌ Failed: ${failed}`);
// Cleanup
sessionStorage.removeItem('xactions_joined_communities');
sessionStorage.removeItem('xactions_join_index');
return;
}
// Check limits
if (CONFIG.maxJoin > 0) {
const successCount = getJoinedCommunities().filter(c => c.status === 'joined').length;
if (successCount >= CONFIG.maxJoin) {
console.log(`🛑 Reached limit of ${CONFIG.maxJoin} communities. Stopping.`);
return;
}
}
const communityId = CONFIG.communities[index];
const urlMatch = window.location.href.match(/\/i\/communities\/(\d+)/);
const currentId = urlMatch ? urlMatch[1] : null;
// Navigate to community if not there
if (currentId !== communityId) {
console.log(`📍 Navigating to community ${index + 1}/${CONFIG.communities.length}: ${communityId}`);
console.log('💡 Re-paste this script after the page loads to continue (progress is saved).');
window.location.href = `https://x.com/i/communities/${communityId}`;
return;
}
console.log(`🔍 Processing community ${index + 1}/${CONFIG.communities.length}: ${communityId}`);
await sleep(CONFIG.navigationDelay);
// Check if already joined
const joinedBtn = document.querySelector($joinedButton);
if (joinedBtn && CONFIG.skipAlreadyJoined) {
console.log(`⏭️ Already a member, skipping...`);
markAsJoined(communityId, 'already_joined');
setCurrentIndex(index + 1);
await sleep(1000);
return processCurrentCommunity();
}
// Check if pending
const pendingBtn = document.querySelector($pendingButton);
if (pendingBtn) {
console.log(`⏳ Request pending, skipping...`);
markAsJoined(communityId, 'pending');
setCurrentIndex(index + 1);
await sleep(1000);
return processCurrentCommunity();
}
// Try to join
const joinBtn = document.querySelector($joinButton);
if (joinBtn) {
console.log(`👆 Clicking Join button...`);
joinBtn.click();
await sleep(CONFIG.joinDelay);
// Check result
const nowJoined = document.querySelector($joinedButton);
const nowPending = document.querySelector($pendingButton);
if (nowJoined) {
console.log(`✅ Successfully joined community: ${communityId}`);
markAsJoined(communityId, 'joined');
} else if (nowPending) {
console.log(`⏳ Request sent (awaiting approval): ${communityId}`);
markAsJoined(communityId, 'pending');
} else {
console.log(`❓ Join status unknown for: ${communityId}`);
markAsJoined(communityId, 'unknown');
}
} else {
console.log(`❌ Join button not found for: ${communityId}`);
markAsJoined(communityId, 'failed');
}
// Move to next
setCurrentIndex(index + 1);
await sleep(CONFIG.joinDelay);
return processCurrentCommunity();
};
processCurrentCommunity();
})();
});
register("keyword-follow", function(){
var CONFIG = {
// ---- LIMITS ----
// Maximum users to follow
maxFollows: 20,
// Max scrolls
maxScrolls: 30,
// ---- FILTERS ----
filters: {
// Skip protected accounts
skipProtected: true,
// Skip accounts that already follow you
skipMutuals: false,
// Skip verified accounts
skipVerified: false,
// Bio must contain at least one of these keywords
// 💡 Leave empty to follow anyone in search results
bioMustContain: [],
// Bio must NOT contain these keywords
bioBlacklist: ['bot', 'automated', 'promo', 'giveaway']
},
// ---- TRACKING ----
// The script tracks who you followed and when, enabling smart unfollow later
trackFollows: true,
// ---- TIMING ----
minDelay: 2000,
maxDelay: 5000,
scrollDelay: 2000
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function keywordFollow() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay) + CONFIG.minDelay);
const $userCell = '[data-testid="UserCell"]';
const $followButton = '[data-testid$="-follow"]';
const $followsYou = '[data-testid="userFollowIndicator"]';
const $protectedIcon = '[data-testid="icon-lock"]';
const $verifiedBadge = '[data-testid="icon-verified"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔑 KEYWORD FOLLOW ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on search page
if (!window.location.href.includes('/search')) {
console.error('❌ ERROR: Must be on a search results page!');
console.log('📍 Go to: https://x.com/search?q=YOUR_KEYWORD&f=user');
return;
}
// Get search query
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q') || 'unknown';
console.log(`🔍 Search query: "${query}"`);
console.log(`📊 Max follows: ${CONFIG.maxFollows}`);
console.log('');
const STORAGE_KEY = 'xactions_keyword_followed';
const TRACKING_KEY = 'xactions_follow_tracking';
const followedUsers = new Set();
let trackingData = {};
// Load existing data
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) JSON.parse(saved).forEach(u => followedUsers.add(u));
const tracking = localStorage.getItem(TRACKING_KEY);
if (tracking) trackingData = JSON.parse(tracking);
} catch (e) {}
let totalFollowed = 0;
let scrolls = 0;
/**
* Get user info
*/
function getUserInfo(cell) {
const link = cell.querySelector('a[href^="/"]');
const username = link ? link.getAttribute('href').replace('/', '').split('/')[0] : null;
// Get bio
const bioEl = cell.querySelector('[dir="auto"]:not([role])');
const bio = bioEl ? bioEl.innerText.toLowerCase() : '';
return { username, bio };
}
/**
* Check filters
*/
function passesFilters(cell, bio) {
if (CONFIG.filters.skipProtected && cell.querySelector($protectedIcon)) return false;
if (CONFIG.filters.skipVerified && cell.querySelector($verifiedBadge)) return false;
if (CONFIG.filters.skipMutuals && cell.querySelector($followsYou)) return false;
// Bio whitelist
if (CONFIG.filters.bioMustContain.length > 0) {
const has = CONFIG.filters.bioMustContain.some(k => bio.includes(k.toLowerCase()));
if (!has) return false;
}
// Bio blacklist
if (CONFIG.filters.bioBlacklist.length > 0) {
const has = CONFIG.filters.bioBlacklist.some(k => bio.includes(k.toLowerCase()));
if (has) return false;
}
return true;
}
console.log('🚀 Starting keyword follow...');
console.log('');
while (totalFollowed < CONFIG.maxFollows && scrolls < CONFIG.maxScrolls) {
const cells = document.querySelectorAll($userCell);
for (const cell of cells) {
if (totalFollowed >= CONFIG.maxFollows) break;
const { username, bio } = getUserInfo(cell);
if (!username || followedUsers.has(username)) continue;
if (!passesFilters(cell, bio)) continue;
const followBtn = cell.querySelector($followButton);
if (!followBtn) continue;
try {
followBtn.click();
followedUsers.add(username);
totalFollowed++;
console.log(`✅ Followed #${totalFollowed}: @${username}`);
// Track follow with timestamp
if (CONFIG.trackFollows) {
trackingData[username] = {
followedAt: new Date().toISOString(),
keyword: query
};
localStorage.setItem(TRACKING_KEY, JSON.stringify(trackingData));
}
localStorage.setItem(STORAGE_KEY, JSON.stringify([...followedUsers]));
await sleep(randomDelay());
} catch (e) {
console.warn('⚠️ Error:', e.message);
}
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ KEYWORD FOLLOW COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`👥 Total followed: ${totalFollowed}`);
console.log(`📊 Total tracked: ${Object.keys(trackingData).length}`);
console.log('');
console.log('💡 Use smart-unfollow.js later to unfollow non-followers!');
console.log('');
return { followed: totalFollowed };
})();
});
register("leave-all-communities", function(){
var CONFIG = {
// Delay before clicking leave button
leaveDelay: 1500,
// Delay after confirming leave
confirmDelay: 2000,
// Delay for navigation
navDelay: 2500,
// Maximum communities to leave (0 = all)
maxToLeave: 0
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
// Stop flag lives on window (not inside the recursive function) so it
// survives every re-invocation of leaveAllCommunities() below.
window.__xaStopLeaveCommunities = false;
window.stopLeaveCommunities = () => {
window.__xaStopLeaveCommunities = true;
console.log('🛑 [Leave Communities] Stop requested. Finishing the current step, then exiting.');
};
(async function leaveAllCommunities() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $communityLink = 'a[href^="/i/communities/"]';
const $joinedButton = 'button[aria-label^="Joined"]';
const $confirmButton = '[data-testid="confirmationSheetConfirm"]';
const $backButton = '[data-testid="app-bar-back"]';
// State management using sessionStorage (survives navigation)
const STORAGE_KEY = 'xactions_left_communities';
const getLeftCommunities = () => {
try {
return JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '[]');
} catch { return []; }
};
const markAsLeft = (id) => {
const left = getLeftCommunities();
if (!left.includes(id)) {
left.push(id);
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(left));
}
};
const isAlreadyLeft = (id) => getLeftCommunities().includes(id);
if (window.__xaStopLeaveCommunities) {
console.log('🛑 Stopped by user. Run the script again to resume where it left off.');
return;
}
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🚪 LEAVE ALL COMMUNITIES ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('💡 To stop early: window.stopLeaveCommunities()');
console.log('');
const leftCount = getLeftCommunities().length;
if (leftCount > 0) {
console.log(`📊 Previously left: ${leftCount} communities`);
console.log('');
}
// Check current page state
const currentUrl = window.location.href;
// Are we inside a community page?
if (currentUrl.includes('/i/communities/') && !currentUrl.endsWith('/communities')) {
console.log('📍 Inside a community page...');
// Extract community ID from URL
const match = currentUrl.match(/\/i\/communities\/(\d+)/);
const communityId = match ? match[1] : null;
// Find and click the "Joined" button
const joinedBtn = document.querySelector($joinedButton);
if (joinedBtn) {
console.log('🔍 Found "Joined" button, clicking...');
joinedBtn.click();
await sleep(CONFIG.leaveDelay);
// Click confirm
const confirmBtn = document.querySelector($confirmButton);
if (confirmBtn) {
confirmBtn.click();
console.log('✅ Left community!');
if (communityId) markAsLeft(communityId);
await sleep(CONFIG.confirmDelay);
} else {
// Mark as processed anyway so we don't re-enter this community forever
console.log('⚠️ Confirmation dialog not found. Skipping this community.');
if (communityId) markAsLeft(communityId);
}
} else {
// No Joined button (admins cannot leave, or the page failed to load).
// Mark as processed so the script does not loop back here forever.
console.log('⚠️ "Joined" button not found (admin or load issue). Skipping this community.');
if (communityId) markAsLeft(communityId);
}
// Navigate back to communities list
const backBtn = document.querySelector($backButton);
if (backBtn && !window.__xaStopLeaveCommunities) {
console.log('🔙 Navigating back...');
backBtn.click();
await sleep(CONFIG.navDelay);
// Re-run the script to continue
console.log('🔄 Continuing to next community...');
leaveAllCommunities();
}
return;
}
// We're on the communities list page
console.log('📍 On communities list page...');
console.log('🔍 Looking for communities to leave...');
// Find all community links
const communityLinks = document.querySelectorAll($communityLink);
if (communityLinks.length === 0) {
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ ALL DONE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 Total communities left: ${getLeftCommunities().length}`);
// Clear storage
sessionStorage.removeItem(STORAGE_KEY);
console.log('🧹 Cleared session storage.');
return;
}
// Find next community to leave
for (const link of communityLinks) {
if (window.__xaStopLeaveCommunities) {
console.log('🛑 Stopped by user. Run the script again to resume where it left off.');
return;
}
const href = link.getAttribute('href');
const match = href?.match(/\/i\/communities\/(\d+)/);
const communityId = match ? match[1] : null;
if (!communityId) continue;
if (isAlreadyLeft(communityId)) continue;
// Check limit
if (CONFIG.maxToLeave > 0 && getLeftCommunities().length >= CONFIG.maxToLeave) {
console.log(`✅ Reached limit of ${CONFIG.maxToLeave} communities.`);
sessionStorage.removeItem(STORAGE_KEY);
return;
}
console.log(`➡️ Entering community: ${communityId}`);
link.click();
await sleep(CONFIG.navDelay);
// The page will navigate, then re-run to leave
console.log('🔄 Page navigating... script will continue.');
leaveAllCommunities();
return;
}
// All visible communities processed
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ ALL DONE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 Total communities left: ${getLeftCommunities().length}`);
sessionStorage.removeItem(STORAGE_KEY);
})();
});
register("leave-community", function(){
var CONFIG = {
// Community ID to leave (get from URL: x.com/i/communities/1234567890)
// Set to null to leave the community you're currently viewing
communityId: null,
// Delay for confirmations (ms)
confirmDelay: 1500,
// Delay for navigation (ms)
navigationDelay: 2500
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function leaveCommunity() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $communityLinks = 'a[href^="/i/communities/"]';
const $joinedButton = 'button[aria-label^="Joined"]';
const $confirmButton = '[data-testid="confirmationSheetConfirm"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🚪 LEAVE SPECIFIC COMMUNITY ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Check if we're already on a community page
const urlMatch = window.location.href.match(/\/i\/communities\/(\d+)/);
const currentCommunityId = urlMatch ? urlMatch[1] : null;
// Determine which community to leave
let targetId = CONFIG.communityId || currentCommunityId;
if (!targetId) {
console.error('❌ ERROR: No community specified!');
console.log('');
console.log('📋 Options:');
console.log(' 1. Go directly to a community page');
console.log(' 2. Set CONFIG.communityId = "YOUR_COMMUNITY_ID"');
console.log('');
console.log('💡 Find community ID in the URL: x.com/i/communities/1234567890');
return;
}
console.log(`🎯 Target community: ${targetId}`);
// Navigate to community if not already there
if (currentCommunityId !== targetId) {
console.log('📍 Navigating to community...');
window.location.href = `https://x.com/i/communities/${targetId}`;
return; // Script will need to be run again after navigation
}
// Look for the Joined button
console.log('🔍 Looking for Joined button...');
await sleep(CONFIG.confirmDelay);
const joinedBtn = document.querySelector($joinedButton);
if (!joinedBtn) {
console.error('❌ ERROR: Joined button not found!');
console.log('');
console.log('📋 Possible reasons:');
console.log(' • You\'re not a member of this community');
console.log(' • The page hasn\'t fully loaded (try again)');
console.log(' • You may need to scroll down');
return;
}
console.log('👆 Clicking Joined button...');
joinedBtn.click();
await sleep(CONFIG.confirmDelay);
// Click confirmation
const confirmBtn = document.querySelector($confirmButton);
if (confirmBtn) {
console.log('✅ Confirming leave...');
confirmBtn.click();
await sleep(CONFIG.confirmDelay);
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(`║ 🎉 Successfully left community: ${targetId} `);
console.log('╚════════════════════════════════════════════════════════════╝');
} else {
console.warn('⚠️ Confirmation button not found. You may need to confirm manually.');
}
})();
});
register("like-by-feed", function(){
(async function likeByFeed() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of tweets to like
maxLikes: 50,
// Skip reply tweets
skipReplies: true,
// Skip promoted/ad tweets
skipAds: true,
// Skip retweets
skipRetweets: true,
// Only like tweets with images/videos
onlyWithMedia: false,
// Minimum delay between actions (ms)
minDelay: 1500,
// Maximum delay between actions (ms)
maxDelay: 3500,
// Maximum scroll attempts to find new tweets
maxScrollAttempts: 20,
// Stop if no new tweets found after this many scrolls
noNewTweetsThreshold: 5
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
tweetText: '[data-testid="tweetText"]',
retweetIndicator: '[data-testid="socialContext"]',
replyIndicator: 'div[data-testid="Tweet-User-Avatar"] + div a[href*="/status/"]',
promotedLabel: '[data-testid="placementTracking"]',
tweetMedia: '[data-testid="tweetPhoto"], [data-testid="videoPlayer"], [data-testid="videoComponent"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => {
window.scrollBy(0, window.innerHeight * 0.7);
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} tweets liked`)
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
liked: 0,
skippedReplies: 0,
skippedAds: 0,
skippedRetweets: 0,
skippedNoMedia: 0,
alreadyLiked: 0,
errors: 0
};
const processedTweets = new Set();
// Stop switch: run window.stopLikeByFeed() from the console to abort
// the loop after the tweet currently being processed.
let stopped = false;
window.stopLikeByFeed = () => {
stopped = true;
log.warning('Stop requested. Finishing the current tweet, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🏠 LIKE BY FEED - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Verify we're on the home page
if (!window.location.href.includes('x.com/home') && !window.location.href.includes('twitter.com/home')) {
log.warning('Not on home timeline. Redirecting...');
window.location.href = 'https://x.com/home';
return;
}
log.info(`Starting feed liker`);
log.info(`Max likes: ${CONFIG.maxLikes}`);
log.info(`Skip replies: ${CONFIG.skipReplies}`);
log.info(`Skip ads: ${CONFIG.skipAds}`);
log.info(`Skip retweets: ${CONFIG.skipRetweets}`);
log.info(`To stop early: window.stopLikeByFeed()`);
let scrollAttempts = 0;
let noNewTweetsCount = 0;
const isReply = (tweet) => {
// Structural marker first (locale-independent), then the English UI text
if (tweet.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(tweet.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
};
const isAd = (tweet) => {
// Check for promoted/ad indicators. Only match exact "Ad"/"Promoted" label
// spans; a substring check on the whole tweet also hits words like "Advice".
if (tweet.querySelector(SELECTORS.promotedLabel) !== null) return true;
return Array.from(tweet.querySelectorAll('span')).some(el => {
const t = el.textContent.trim();
return t === 'Ad' || t === 'Promoted';
});
};
const isRetweet = (tweet) => {
// Reposts render socialContext inside an <a>; pinned posts render it as a
// plain element. Checking the tag keeps pinned posts from matching.
const socialContext = tweet.querySelector(SELECTORS.retweetIndicator);
return !!socialContext && socialContext.closest('a') !== null;
};
const hasMedia = (tweet) => {
return tweet.querySelector(SELECTORS.tweetMedia) !== null;
};
const getTweetIdentifier = (tweet) => {
// Prefer the permalink around the timestamp: the first /status/ link in
// the article can belong to a quoted tweet and give the wrong ID
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const links = tweet.querySelectorAll('a[href*="/status/"]');
for (const link of links) {
const match = link.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
// Fallback to text content hash
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
return text.substring(0, 100);
};
while (!stopped && stats.liked < CONFIG.maxLikes && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewTweet = false;
for (const tweet of tweets) {
if (stopped || stats.liked >= CONFIG.maxLikes) break;
const tweetId = getTweetIdentifier(tweet);
if (processedTweets.has(tweetId)) continue;
processedTweets.add(tweetId);
foundNewTweet = true;
try {
// Skip replies if configured
if (CONFIG.skipReplies && isReply(tweet)) {
stats.skippedReplies++;
log.info('Skipped reply tweet');
continue;
}
// Skip ads if configured
if (CONFIG.skipAds && isAd(tweet)) {
stats.skippedAds++;
log.info('Skipped promoted/ad tweet');
continue;
}
// Skip retweets if configured
if (CONFIG.skipRetweets && isRetweet(tweet)) {
stats.skippedRetweets++;
log.info('Skipped retweet');
continue;
}
// Only with media check
if (CONFIG.onlyWithMedia && !hasMedia(tweet)) {
stats.skippedNoMedia++;
continue;
}
// Check if already liked
const unlikeButton = tweet.querySelector(SELECTORS.unlikeButton);
if (unlikeButton) {
stats.alreadyLiked++;
continue;
}
// Find and click like button
const likeButton = tweet.querySelector(SELECTORS.likeButton);
if (likeButton) {
// Scroll tweet into view
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
likeButton.click();
stats.liked++;
const preview = tweet.querySelector(SELECTORS.tweetText)?.textContent?.substring(0, 50) || 'No text';
log.success(`Liked tweet #${stats.liked}: "${preview}..."`);
log.progress(stats.liked, CONFIG.maxLikes);
await randomDelay();
}
} catch (error) {
log.error(`Error processing tweet: ${error.message}`);
stats.errors++;
}
}
if (!foundNewTweet) {
noNewTweetsCount++;
if (noNewTweetsCount >= CONFIG.noNewTweetsThreshold) {
log.warning('No new tweets found after multiple scrolls. Stopping.');
break;
}
} else {
noNewTweetsCount = 0;
}
// Scroll for more tweets
scrollDown();
scrollAttempts++;
log.info(`Scrolling for more tweets... (attempt ${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
// ============================================
// 📊 SUMMARY
// ============================================
const totalSkipped = stats.skippedReplies + stats.skippedAds + stats.skippedRetweets + stats.skippedNoMedia;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 LIKE BY FEED - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Total Liked: ${String(stats.liked).padEnd(32)}║
║ ⏭️ Total Skipped: ${String(totalSkipped).padEnd(32)}║
║ └─ Replies: ${String(stats.skippedReplies).padEnd(32)}║
║ └─ Ads: ${String(stats.skippedAds).padEnd(32)}║
║ └─ Retweets: ${String(stats.skippedRetweets).padEnd(32)}║
║ └─ No Media: ${String(stats.skippedNoMedia).padEnd(32)}║
║ 💗 Already Liked: ${String(stats.alreadyLiked).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
║ 📜 Scroll Attempts: ${String(scrollAttempts).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("like-by-hashtag", function(){
(async function likeByHashtag() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Hashtags to search for (without the # symbol)
hashtags: ['javascript', 'webdev', 'coding'],
// Maximum number of tweets to like per hashtag
maxLikesPerHashtag: 10,
// Total maximum likes across all hashtags
maxTotalLikes: 30,
// Minimum delay between actions (ms)
minDelay: 2000,
// Maximum delay between actions (ms)
maxDelay: 4000,
// Skip retweets
skipRetweets: true,
// Skip tweets with media only (no text)
skipMediaOnly: false,
// Scroll attempts before moving to next hashtag
maxScrollAttempts: 5
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
tweetText: '[data-testid="tweetText"]',
retweetIndicator: '[data-testid="socialContext"]',
searchBox: '[data-testid="SearchBox_Search_Input"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => {
window.scrollBy(0, window.innerHeight * 0.8);
};
// Navigate within the SPA. Assigning window.location.href triggers a full
// page load, which destroys this console script before it can continue.
const spaNavigate = (url) => {
try {
const target = new URL(url, window.location.href);
if (target.origin === window.location.origin) {
window.history.pushState({}, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
return;
}
} catch (e) {}
window.location.href = url;
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} tweets liked`)
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
totalLiked: 0,
skipped: 0,
alreadyLiked: 0,
errors: 0,
byHashtag: {}
};
const processedTweets = new Set();
// Stop switch: run window.stopLikeByHashtag() from the console to abort
// the loop after the tweet currently being processed.
let stopped = false;
window.stopLikeByHashtag = () => {
stopped = true;
log.warning('Stop requested. Finishing the current tweet, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🏷️ LIKE BY HASHTAG - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
log.info(`Starting hashtag liker for: #${CONFIG.hashtags.join(', #')}`);
log.info(`Max likes per hashtag: ${CONFIG.maxLikesPerHashtag}`);
log.info(`Max total likes: ${CONFIG.maxTotalLikes}`);
log.info(`To stop early: window.stopLikeByHashtag()`);
const likeTweetsOnPage = async (hashtag) => {
let hashtagLikes = 0;
let scrollAttempts = 0;
let noNewTweetsCount = 0;
while (!stopped && hashtagLikes < CONFIG.maxLikesPerHashtag &&
stats.totalLiked < CONFIG.maxTotalLikes &&
scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewTweet = false;
for (const tweet of tweets) {
if (stopped || hashtagLikes >= CONFIG.maxLikesPerHashtag || stats.totalLiked >= CONFIG.maxTotalLikes) {
break;
}
// Unique tweet ID from the permalink around the timestamp (the first
// /status/ link can belong to a quoted tweet); text is the fallback
const tweetText = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
const idMatch = timeAnchor?.href.match(/\/status\/(\d+)/);
const tweetId = idMatch ? idMatch[1] : tweetText.substring(0, 100);
if (processedTweets.has(tweetId)) {
continue;
}
processedTweets.add(tweetId);
foundNewTweet = true;
try {
// Skip retweets if configured (socialContext inside an <a> = repost;
// a plain socialContext is a pinned post, not a repost)
const socialContext = tweet.querySelector(SELECTORS.retweetIndicator);
if (CONFIG.skipRetweets && socialContext && socialContext.closest('a') !== null) {
stats.skipped++;
continue;
}
// Skip media-only tweets if configured
if (CONFIG.skipMediaOnly && !tweetText.trim()) {
stats.skipped++;
continue;
}
// Check if already liked
const unlikeButton = tweet.querySelector(SELECTORS.unlikeButton);
if (unlikeButton) {
stats.alreadyLiked++;
continue;
}
// Find and click like button
const likeButton = tweet.querySelector(SELECTORS.likeButton);
if (likeButton) {
likeButton.click();
hashtagLikes++;
stats.totalLiked++;
if (!stats.byHashtag[hashtag]) {
stats.byHashtag[hashtag] = 0;
}
stats.byHashtag[hashtag]++;
log.success(`Liked tweet #${stats.totalLiked} for #${hashtag}`);
log.progress(stats.totalLiked, CONFIG.maxTotalLikes);
await randomDelay();
}
} catch (error) {
log.error(`Error processing tweet: ${error.message}`);
stats.errors++;
}
}
if (!foundNewTweet) {
noNewTweetsCount++;
if (noNewTweetsCount >= 3) {
log.warning('No new tweets found after multiple scrolls');
break;
}
} else {
noNewTweetsCount = 0;
}
// Scroll for more tweets
scrollDown();
scrollAttempts++;
await sleep(1500);
}
return hashtagLikes;
};
const navigateToHashtag = async (hashtag) => {
const searchUrl = `https://x.com/search?q=%23${encodeURIComponent(hashtag)}&src=typed_query&f=live`;
spaNavigate(searchUrl);
// Wait for page to load
await sleep(3000);
// Wait for tweets to appear
let attempts = 0;
while (!document.querySelector(SELECTORS.tweet) && attempts < 10) {
await sleep(1000);
attempts++;
}
return attempts < 10;
};
// Process each hashtag
for (const hashtag of CONFIG.hashtags) {
if (stopped) break;
if (stats.totalLiked >= CONFIG.maxTotalLikes) {
log.warning('Reached maximum total likes limit');
break;
}
log.info(`\n🔍 Searching for #${hashtag}...`);
const navigated = await navigateToHashtag(hashtag);
if (!navigated) {
log.error(`Failed to load tweets for #${hashtag}`);
continue;
}
await sleep(2000);
await likeTweetsOnPage(hashtag);
log.info(`Finished processing #${hashtag}`);
await sleep(2000);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 LIKE BY HASHTAG - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Total Liked: ${String(stats.totalLiked).padEnd(34)}║
║ ⏭️ Skipped: ${String(stats.skipped).padEnd(34)}║
║ 💗 Already Liked: ${String(stats.alreadyLiked).padEnd(34)}║
║ ❌ Errors: ${String(stats.errors).padEnd(34)}║
╠══════════════════════════════════════════════════════════╣
║ 📈 Likes by Hashtag: ║
${Object.entries(stats.byHashtag).map(([tag, count]) =>
`║ #${tag}: ${count}`.padEnd(59) + '║'
).join('\n')}
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("like-by-location", function(){
(async function likeByLocation() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Location to search tweets from
// Examples: "New York", "San Francisco, CA", "London", "Tokyo"
location: 'San Francisco',
// Search radius in miles (optional, appended to search)
radiusMiles: 25,
// Optional keyword to combine with location search
// Leave empty to search all tweets from location
keyword: '',
// Maximum number of tweets to like
maxLikes: 30,
// Minimum delay between actions (ms)
minDelay: 2000,
// Maximum delay between actions (ms)
maxDelay: 4000,
// Skip retweets
skipRetweets: true,
// Skip replies
skipReplies: false,
// Maximum scroll attempts
maxScrollAttempts: 15,
// Search type: 'live' (recent) or 'top'
searchType: 'live'
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
tweetText: '[data-testid="tweetText"]',
retweetIndicator: '[data-testid="socialContext"]',
searchResults: '[data-testid="primaryColumn"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => {
window.scrollBy(0, window.innerHeight * 0.75);
};
// Navigate within the SPA. Assigning window.location.href triggers a full
// page load, which destroys this console script before it can continue.
const spaNavigate = (url) => {
try {
const target = new URL(url, window.location.href);
if (target.origin === window.location.origin) {
window.history.pushState({}, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
return;
}
} catch (e) {}
window.location.href = url;
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} tweets liked`)
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
liked: 0,
skippedRetweets: 0,
skippedReplies: 0,
alreadyLiked: 0,
errors: 0,
tweetsProcessed: 0
};
const processedTweets = new Set();
// Stop switch: run window.stopLikeByLocation() from the console to abort
// the loop after the tweet currently being processed.
let stopped = false;
window.stopLikeByLocation = () => {
stopped = true;
log.warning('Stop requested. Finishing the current tweet, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📍 LIKE BY LOCATION - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
log.info(`Location: ${CONFIG.location}`);
log.info(`Radius: ${CONFIG.radiusMiles} miles`);
log.info(`Max likes: ${CONFIG.maxLikes}`);
log.info(`To stop early: window.stopLikeByLocation()`);
// Build the search query
const buildSearchQuery = () => {
let query = '';
if (CONFIG.keyword) {
query += CONFIG.keyword + ' ';
}
query += `near:"${CONFIG.location}"`;
if (CONFIG.radiusMiles) {
query += ` within:${CONFIG.radiusMiles}mi`;
}
return query;
};
const navigateToSearch = async () => {
const query = buildSearchQuery();
const encodedQuery = encodeURIComponent(query);
const searchUrl = `https://x.com/search?q=${encodedQuery}&src=typed_query&f=${CONFIG.searchType}`;
log.info(`Search query: ${query}`);
log.info(`Navigating to search...`);
spaNavigate(searchUrl);
// Wait for page to load
await sleep(3000);
// Wait for tweets to appear
let attempts = 0;
while (!document.querySelector(SELECTORS.tweet) && attempts < 15) {
await sleep(1000);
attempts++;
}
if (attempts >= 15) {
log.error('No tweets found for this location. Try a different location or check spelling.');
return false;
}
return true;
};
const isReply = (tweet) => {
// Structural marker first (locale-independent), then the English UI text
if (tweet.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(tweet.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
};
const isRetweet = (tweet) => {
// Reposts render socialContext inside an <a>; pinned posts render it as a
// plain element. Checking the tag keeps pinned posts from matching.
const socialContext = tweet.querySelector(SELECTORS.retweetIndicator);
return !!socialContext && socialContext.closest('a') !== null;
};
const getTweetIdentifier = (tweet) => {
// Prefer the permalink around the timestamp: the first /status/ link in
// the article can belong to a quoted tweet and give the wrong ID
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const links = tweet.querySelectorAll('a[href*="/status/"]');
for (const link of links) {
const match = link.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
// Text fallback. Do NOT append Date.now(): a changing ID makes the same
// tweet look new on every pass, defeating deduplication entirely.
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
return text.substring(0, 100);
};
const likeTweets = async () => {
let scrollAttempts = 0;
let noNewTweetsCount = 0;
while (!stopped && stats.liked < CONFIG.maxLikes && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewTweet = false;
for (const tweet of tweets) {
if (stopped || stats.liked >= CONFIG.maxLikes) break;
const tweetId = getTweetIdentifier(tweet);
if (processedTweets.has(tweetId)) continue;
processedTweets.add(tweetId);
foundNewTweet = true;
stats.tweetsProcessed++;
try {
// Skip retweets if configured
if (CONFIG.skipRetweets && isRetweet(tweet)) {
stats.skippedRetweets++;
continue;
}
// Skip replies if configured
if (CONFIG.skipReplies && isReply(tweet)) {
stats.skippedReplies++;
continue;
}
// Check if already liked
const unlikeButton = tweet.querySelector(SELECTORS.unlikeButton);
if (unlikeButton) {
stats.alreadyLiked++;
continue;
}
// Find and click like button
const likeButton = tweet.querySelector(SELECTORS.likeButton);
if (likeButton) {
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
likeButton.click();
stats.liked++;
const preview = tweet.querySelector(SELECTORS.tweetText)?.textContent?.substring(0, 40) || 'No text';
log.success(`Liked tweet #${stats.liked} from ${CONFIG.location}: "${preview}..."`);
log.progress(stats.liked, CONFIG.maxLikes);
await randomDelay();
}
} catch (error) {
log.error(`Error processing tweet: ${error.message}`);
stats.errors++;
}
}
if (!foundNewTweet) {
noNewTweetsCount++;
if (noNewTweetsCount >= 4) {
log.warning('No new tweets found. Location may have limited activity.');
break;
}
} else {
noNewTweetsCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Scrolling... (${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
};
// Execute
const pageLoaded = await navigateToSearch();
if (pageLoaded) {
await sleep(2000);
await likeTweets();
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 LIKE BY LOCATION - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 📍 Location: ${String(CONFIG.location).padEnd(32)}║
║ 🔍 Radius: ${String(CONFIG.radiusMiles + ' miles').padEnd(32)}║
╠══════════════════════════════════════════════════════════╣
║ ✅ Total Liked: ${String(stats.liked).padEnd(32)}║
║ 📄 Tweets Processed: ${String(stats.tweetsProcessed).padEnd(32)}║
║ ⏭️ Skipped Retweets: ${String(stats.skippedRetweets).padEnd(32)}║
║ ⏭️ Skipped Replies: ${String(stats.skippedReplies).padEnd(32)}║
║ 💗 Already Liked: ${String(stats.alreadyLiked).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("like-by-user", function(){
(async function likeByUser() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of tweets to like
maxLikes: 50,
// Skip replies by this user
skipReplies: false,
// Skip retweets by this user
skipRetweets: true,
// Skip quote tweets
skipQuoteTweets: false,
// Only like tweets with images/videos
onlyWithMedia: false,
// Only like tweets with minimum engagement
minLikes: 0,
minRetweets: 0,
// Minimum delay between actions (ms)
minDelay: 1500,
// Maximum delay between actions (ms)
maxDelay: 3500,
// Maximum scroll attempts
maxScrollAttempts: 25,
// Stop after this many consecutive already-liked tweets
stopAfterAlreadyLiked: 10
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
tweetText: '[data-testid="tweetText"]',
retweetIndicator: '[data-testid="socialContext"]',
tweetMedia: '[data-testid="tweetPhoto"], [data-testid="videoPlayer"], [data-testid="videoComponent"]',
userAvatar: '[data-testid="Tweet-User-Avatar"]',
likeCount: '[data-testid="like"] span, [data-testid="unlike"] span',
retweetCount: '[data-testid="retweet"] span'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => {
window.scrollBy(0, window.innerHeight * 0.7);
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} tweets liked`)
};
const parseCount = (text) => {
if (!text) return 0;
const cleaned = text.replace(/,/g, '').trim();
if (cleaned.endsWith('K')) {
return parseFloat(cleaned) * 1000;
}
if (cleaned.endsWith('M')) {
return parseFloat(cleaned) * 1000000;
}
if (cleaned.endsWith('B')) {
return parseFloat(cleaned) * 1000000000;
}
return parseInt(cleaned) || 0;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
liked: 0,
skippedReplies: 0,
skippedRetweets: 0,
skippedQuotes: 0,
skippedNoMedia: 0,
skippedLowEngagement: 0,
alreadyLiked: 0,
errors: 0,
tweetsProcessed: 0
};
const processedTweets = new Set();
// Stop switch: run window.stopLikeByUser() from the console to abort
// the loop after the tweet currently being processed.
let stopped = false;
window.stopLikeByUser = () => {
stopped = true;
log.warning('Stop requested. Finishing the current tweet, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 👤 LIKE BY USER - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Get current username from URL
const currentUrl = window.location.href;
const usernameMatch = currentUrl.match(/x\.com\/([^\/\?]+)|twitter\.com\/([^\/\?]+)/);
const username = usernameMatch ? (usernameMatch[1] || usernameMatch[2]) : 'Unknown';
// Verify we're on a profile page
if (username === 'home' || username === 'explore' || username === 'search' || username === 'notifications') {
log.error('Please navigate to a user profile page first (x.com/username)');
log.info('Example: Go to x.com/elonmusk to like their tweets');
return;
}
log.info(`Target user: @${username}`);
log.info(`Max likes: ${CONFIG.maxLikes}`);
log.info(`Skip replies: ${CONFIG.skipReplies}`);
log.info(`Skip retweets: ${CONFIG.skipRetweets}`);
log.info(`To stop early: window.stopLikeByUser()`);
const isReply = (tweet) => {
// Structural marker first (locale-independent), then the English UI text
if (tweet.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(tweet.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
};
const isRetweet = (tweet) => {
// Reposts render socialContext inside an <a> linking to the reposter;
// pinned posts render it as a plain element. The structural check works
// on every UI language, unlike matching "reposted"/"Retweeted" text.
const socialContext = tweet.querySelector(SELECTORS.retweetIndicator);
return !!socialContext && socialContext.closest('a') !== null;
};
const isQuoteTweet = (tweet) => {
// A quote tweet embeds the quoted post as a card inside a
// div[role="link"] that itself contains a <time> element. The card does
// NOT carry a nested data-testid="tweet", so matching on that (as this
// used to) never found anything and skipQuoteTweets silently did nothing.
return tweet.querySelector('div[role="link"] time') !== null;
};
const hasMedia = (tweet) => {
return tweet.querySelector(SELECTORS.tweetMedia) !== null;
};
const getEngagement = (tweet) => {
const likeSpan = tweet.querySelector(SELECTORS.likeCount);
const retweetSpan = tweet.querySelector(SELECTORS.retweetCount);
return {
likes: parseCount(likeSpan?.textContent),
retweets: parseCount(retweetSpan?.textContent)
};
};
const getTweetIdentifier = (tweet) => {
// Prefer the permalink around the timestamp: the first /status/ link in
// the article can belong to a quoted tweet and give the wrong ID
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const links = tweet.querySelectorAll('a[href*="/status/"]');
for (const link of links) {
const match = link.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
// Text fallback. Do NOT append Date.now(): a changing ID makes the same
// tweet look new on every pass, defeating deduplication entirely.
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
return text.substring(0, 100);
};
let scrollAttempts = 0;
let noNewTweetsCount = 0;
let consecutiveAlreadyLiked = 0;
while (!stopped && stats.liked < CONFIG.maxLikes &&
scrollAttempts < CONFIG.maxScrollAttempts &&
consecutiveAlreadyLiked < CONFIG.stopAfterAlreadyLiked) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewTweet = false;
for (const tweet of tweets) {
if (stopped || stats.liked >= CONFIG.maxLikes) break;
if (consecutiveAlreadyLiked >= CONFIG.stopAfterAlreadyLiked) break;
const tweetId = getTweetIdentifier(tweet);
if (processedTweets.has(tweetId)) continue;
processedTweets.add(tweetId);
foundNewTweet = true;
stats.tweetsProcessed++;
try {
// Skip replies if configured
if (CONFIG.skipReplies && isReply(tweet)) {
stats.skippedReplies++;
log.info('Skipped reply');
continue;
}
// Skip retweets if configured
if (CONFIG.skipRetweets && isRetweet(tweet)) {
stats.skippedRetweets++;
log.info('Skipped retweet');
continue;
}
// Skip quote tweets if configured
if (CONFIG.skipQuoteTweets && isQuoteTweet(tweet)) {
stats.skippedQuotes++;
log.info('Skipped quote tweet');
continue;
}
// Only with media check
if (CONFIG.onlyWithMedia && !hasMedia(tweet)) {
stats.skippedNoMedia++;
continue;
}
// Engagement check
if (CONFIG.minLikes > 0 || CONFIG.minRetweets > 0) {
const engagement = getEngagement(tweet);
if (engagement.likes < CONFIG.minLikes || engagement.retweets < CONFIG.minRetweets) {
stats.skippedLowEngagement++;
continue;
}
}
// Check if already liked
const unlikeButton = tweet.querySelector(SELECTORS.unlikeButton);
if (unlikeButton) {
stats.alreadyLiked++;
consecutiveAlreadyLiked++;
if (consecutiveAlreadyLiked >= CONFIG.stopAfterAlreadyLiked) {
log.warning(`Found ${consecutiveAlreadyLiked} consecutive already-liked tweets. Stopping to avoid duplicates.`);
}
continue;
}
// Reset consecutive counter when we find an unliked tweet
consecutiveAlreadyLiked = 0;
// Find and click like button
const likeButton = tweet.querySelector(SELECTORS.likeButton);
if (likeButton) {
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
likeButton.click();
stats.liked++;
const preview = tweet.querySelector(SELECTORS.tweetText)?.textContent?.substring(0, 45) || 'Media/No text';
log.success(`Liked @${username}'s tweet #${stats.liked}: "${preview}..."`);
log.progress(stats.liked, CONFIG.maxLikes);
await randomDelay();
}
} catch (error) {
log.error(`Error processing tweet: ${error.message}`);
stats.errors++;
}
}
if (!foundNewTweet) {
noNewTweetsCount++;
if (noNewTweetsCount >= 5) {
log.warning('No new tweets found. User may have limited content.');
break;
}
} else {
noNewTweetsCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Scrolling... (${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 LIKE BY USER - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 👤 Target User: @${String(username).padEnd(31)}║
╠══════════════════════════════════════════════════════════╣
║ ✅ Total Liked: ${String(stats.liked).padEnd(32)}║
║ 📄 Tweets Processed: ${String(stats.tweetsProcessed).padEnd(32)}║
║ ⏭️ Skipped Replies: ${String(stats.skippedReplies).padEnd(32)}║
║ ⏭️ Skipped Retweets: ${String(stats.skippedRetweets).padEnd(32)}║
║ ⏭️ Skipped Quotes: ${String(stats.skippedQuotes).padEnd(32)}║
║ ⏭️ Low Engagement: ${String(stats.skippedLowEngagement).padEnd(32)}║
║ 💗 Already Liked: ${String(stats.alreadyLiked).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("like-user-replies", function(){
(async function likeUserReplies() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of replies to like
maxLikes: 30,
// Skip nested replies (replies to replies)
skipNestedReplies: false,
// Only like replies from verified users
onlyVerified: false,
// Only like replies with images/videos
onlyWithMedia: false,
// Skip replies that contain specific words
skipContaining: [],
// Only like replies containing specific words (empty = all)
onlyContaining: [],
// Minimum delay between actions (ms)
minDelay: 1500,
// Maximum delay between actions (ms)
maxDelay: 3500,
// Maximum scroll attempts
maxScrollAttempts: 20,
// Skip the original tweet (only like replies)
skipOriginalTweet: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
likeButton: '[data-testid="like"]',
unlikeButton: '[data-testid="unlike"]',
tweetText: '[data-testid="tweetText"]',
userCell: '[data-testid="User-Name"]',
verifiedBadge: '[data-testid="icon-verified"]',
tweetMedia: '[data-testid="tweetPhoto"], [data-testid="videoPlayer"], [data-testid="videoComponent"]',
conversationThread: '[data-testid="cellInnerDiv"]',
replyingTo: 'div[dir="ltr"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => {
window.scrollBy(0, window.innerHeight * 0.7);
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} replies liked`)
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
liked: 0,
skippedOriginal: 0,
skippedNested: 0,
skippedNotVerified: 0,
skippedNoMedia: 0,
skippedFiltered: 0,
alreadyLiked: 0,
errors: 0,
repliesProcessed: 0
};
const processedTweets = new Set();
// Stop switch: run window.stopLikeUserReplies() from the console to abort
// the loop after the reply currently being processed.
let stopped = false;
window.stopLikeUserReplies = () => {
stopped = true;
log.warning('Stop requested. Finishing the current reply, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 💬 LIKE USER REPLIES - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Verify we're on a tweet page
const currentUrl = window.location.href;
if (!currentUrl.includes('/status/')) {
log.error('Please navigate to a tweet page first (x.com/username/status/xxx)');
log.info('Example: Go to a specific tweet to like its replies');
return;
}
// Extract tweet info from URL
const urlMatch = currentUrl.match(/\/([^\/]+)\/status\/(\d+)/);
const tweetAuthor = urlMatch ? urlMatch[1] : 'Unknown';
const tweetId = urlMatch ? urlMatch[2] : 'Unknown';
log.info(`Tweet by: @${tweetAuthor}`);
log.info(`Tweet ID: ${tweetId}`);
log.info(`Max likes: ${CONFIG.maxLikes}`);
log.info(`Skip original tweet: ${CONFIG.skipOriginalTweet}`);
log.info(`To stop early: window.stopLikeUserReplies()`);
const isVerified = (tweet) => {
return tweet.querySelector(SELECTORS.verifiedBadge) !== null;
};
const hasMedia = (tweet) => {
return tweet.querySelector(SELECTORS.tweetMedia) !== null;
};
const getTweetText = (tweet) => {
const textElement = tweet.querySelector(SELECTORS.tweetText);
return textElement ? textElement.textContent.toLowerCase() : '';
};
const containsFilteredWords = (text) => {
if (CONFIG.skipContaining.length === 0) return false;
return CONFIG.skipContaining.some(word => text.includes(word.toLowerCase()));
};
const containsRequiredWords = (text) => {
if (CONFIG.onlyContaining.length === 0) return true;
return CONFIG.onlyContaining.some(word => text.includes(word.toLowerCase()));
};
const isOriginalTweet = (tweet, index, replyId) => {
// Structural check first: a tweet whose own permalink ID matches the
// /status/ ID in the URL is the original, on any UI language
if (replyId === tweetId) return true;
// The original tweet is usually the first one on the page
// and doesn't have "Replying to" text
if (index === 0) return true;
const tweetContent = tweet.textContent || '';
// Check if it's NOT a reply (original tweets don't show "Replying to")
const hasReplyingTo = tweetContent.includes('Replying to');
return !hasReplyingTo && index < 2;
};
const isNestedReply = (tweet) => {
// Check if this reply is replying to another reply (not the original tweet)
const tweetContent = tweet.textContent || '';
if (!tweetContent.includes('Replying to')) return false;
// This is a simple heuristic - if replying to multiple users, it's likely nested
const matches = tweetContent.match(/Replying to/g);
return matches && matches.length > 1;
};
const getReplyIdentifier = (tweet) => {
// Prefer the permalink around the timestamp: the first /status/ link in
// the article can belong to a quoted tweet and give the wrong ID
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const links = tweet.querySelectorAll('a[href*="/status/"]');
for (const link of links) {
const match = link.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
// Text fallback. Do NOT append Date.now(): a changing ID makes the same
// reply look new on every pass, defeating deduplication entirely.
const text = tweet.querySelector(SELECTORS.tweetText)?.textContent || '';
return text.substring(0, 100);
};
const getUsername = (tweet) => {
const userCell = tweet.querySelector(SELECTORS.userCell);
if (!userCell) return 'Unknown';
const link = userCell.querySelector('a[href^="/"]');
if (!link) return 'Unknown';
return link.getAttribute('href')?.replace('/', '') || 'Unknown';
};
let scrollAttempts = 0;
let noNewRepliesCount = 0;
let isFirstBatch = true;
// Initial scroll to load replies
await sleep(2000);
while (!stopped && stats.liked < CONFIG.maxLikes && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewReply = false;
let tweetIndex = 0;
for (const tweet of tweets) {
if (stopped || stats.liked >= CONFIG.maxLikes) break;
const replyId = getReplyIdentifier(tweet);
if (processedTweets.has(replyId)) {
tweetIndex++;
continue;
}
processedTweets.add(replyId);
foundNewReply = true;
try {
// Skip original tweet if configured
if (CONFIG.skipOriginalTweet && isFirstBatch && isOriginalTweet(tweet, tweetIndex, replyId)) {
stats.skippedOriginal++;
log.info('Skipped original tweet');
tweetIndex++;
continue;
}
stats.repliesProcessed++;
// Skip nested replies if configured
if (CONFIG.skipNestedReplies && isNestedReply(tweet)) {
stats.skippedNested++;
log.info('Skipped nested reply');
tweetIndex++;
continue;
}
// Only verified check
if (CONFIG.onlyVerified && !isVerified(tweet)) {
stats.skippedNotVerified++;
tweetIndex++;
continue;
}
// Only with media check
if (CONFIG.onlyWithMedia && !hasMedia(tweet)) {
stats.skippedNoMedia++;
tweetIndex++;
continue;
}
// Word filter check
const tweetText = getTweetText(tweet);
if (containsFilteredWords(tweetText)) {
stats.skippedFiltered++;
log.info('Skipped reply (contains filtered word)');
tweetIndex++;
continue;
}
if (!containsRequiredWords(tweetText)) {
stats.skippedFiltered++;
tweetIndex++;
continue;
}
// Check if already liked
const unlikeButton = tweet.querySelector(SELECTORS.unlikeButton);
if (unlikeButton) {
stats.alreadyLiked++;
tweetIndex++;
continue;
}
// Find and click like button
const likeButton = tweet.querySelector(SELECTORS.likeButton);
if (likeButton) {
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
likeButton.click();
stats.liked++;
const username = getUsername(tweet);
const preview = tweetText.substring(0, 40) || 'Media/No text';
log.success(`Liked reply #${stats.liked} by @${username}: "${preview}..."`);
log.progress(stats.liked, CONFIG.maxLikes);
await randomDelay();
}
tweetIndex++;
} catch (error) {
log.error(`Error processing reply: ${error.message}`);
stats.errors++;
tweetIndex++;
}
}
isFirstBatch = false;
if (!foundNewReply) {
noNewRepliesCount++;
if (noNewRepliesCount >= 5) {
log.warning('No new replies found. May have reached the end of thread.');
break;
}
} else {
noNewRepliesCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Loading more replies... (${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
// ============================================
// 📊 SUMMARY
// ============================================
const totalSkipped = stats.skippedOriginal + stats.skippedNested + stats.skippedNotVerified +
stats.skippedNoMedia + stats.skippedFiltered;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 LIKE USER REPLIES - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 👤 Tweet Author: @${String(tweetAuthor).padEnd(31)}║
║ 🔗 Tweet ID: ${String(tweetId).padEnd(32)}║
╠══════════════════════════════════════════════════════════╣
║ ✅ Total Liked: ${String(stats.liked).padEnd(32)}║
║ 💬 Replies Processed: ${String(stats.repliesProcessed).padEnd(32)}║
║ ⏭️ Total Skipped: ${String(totalSkipped).padEnd(32)}║
║ └─ Original: ${String(stats.skippedOriginal).padEnd(32)}║
║ └─ Nested: ${String(stats.skippedNested).padEnd(32)}║
║ └─ Not Verified: ${String(stats.skippedNotVerified).padEnd(32)}║
║ └─ No Media: ${String(stats.skippedNoMedia).padEnd(32)}║
║ └─ Filtered: ${String(stats.skippedFiltered).padEnd(32)}║
║ 💗 Already Liked: ${String(stats.alreadyLiked).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("link-scraper", function(){
var CONFIG = {
// Maximum scrolls to load tweets
maxScrolls: 100,
// Maximum tweets to scan
maxTweets: 500,
// Scroll delay
scrollDelay: 1500,
// Include Twitter/X internal links
includeTwitterLinks: false,
// Include media links (images/videos)
includeMedia: false,
// Domains to exclude
excludeDomains: [
't.co' // Twitter's URL shortener (usually just redirects)
],
// Auto-download results
autoDownload: true,
// Max retries when no new content
maxRetries: 5
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function linkScraper() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $tweet = 'article[data-testid="tweet"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔗 LINK SCRAPER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Get username from URL
const pathMatch = window.location.pathname.match(/^\/([^\/]+)/);
const username = pathMatch ? pathMatch[1] : null;
if (!username || ['home', 'explore', 'search', 'notifications', 'messages', 'i'].includes(username)) {
console.error('❌ ERROR: Must be on a user\'s profile page!');
console.log('📍 Go to: https://x.com/username');
return;
}
console.log(`👤 Scraping links from: @${username}`);
console.log('');
const links = new Map(); // url -> { url, domain, tweetCount, tweets }
const seenTweets = new Set();
let scrolls = 0;
let retries = 0;
let lastLinkCount = 0;
let lastTweetCount = 0;
/**
* Extract domain from URL
*/
function getDomain(url) {
try {
return new URL(url).hostname.replace('www.', '');
} catch {
return 'unknown';
}
}
/**
* Check if link should be included
*/
function shouldInclude(url) {
const domain = getDomain(url);
// Exclude configured domains
if (CONFIG.excludeDomains.includes(domain)) return false;
// Exclude Twitter links
if (!CONFIG.includeTwitterLinks) {
if (domain.includes('twitter.com') || domain.includes('x.com')) return false;
}
// Exclude media
if (!CONFIG.includeMedia) {
if (domain.includes('twimg.com') || url.includes('/photo/') || url.includes('/video/')) return false;
}
return true;
}
/**
* Extract links from tweet
*/
function extractLinks(tweetEl) {
// Get tweet ID for deduplication. Prefer the permalink around the
// timestamp: the first /status/ link can belong to a quoted tweet.
const timeAnchor = tweetEl.querySelector('time')?.closest('a[href*="/status/"]');
const tweetLink = timeAnchor || tweetEl.querySelector('a[href*="/status/"]');
if (!tweetLink) return [];
const match = tweetLink.href.match(/\/status\/(\d+)/);
if (!match) return [];
const tweetId = match[1];
if (seenTweets.has(tweetId)) return [];
seenTweets.add(tweetId);
const foundLinks = [];
// Find all links in tweet
tweetEl.querySelectorAll('a').forEach(a => {
const href = a.href;
if (!href || !href.startsWith('http')) return;
// Get expanded URL from title attribute if available
let url = a.getAttribute('title') || href;
// Skip if it's a relative link or twitter internal
if (!url.startsWith('http')) return;
if (shouldInclude(url)) {
foundLinks.push({
url,
domain: getDomain(url),
tweetId,
tweetUrl: `https://x.com/${username}/status/${tweetId}`
});
}
});
return foundLinks;
}
console.log('🚀 Scraping links...');
console.log('');
// Scroll and extract
while (scrolls < CONFIG.maxScrolls && seenTweets.size < CONFIG.maxTweets && retries < CONFIG.maxRetries) {
document.querySelectorAll($tweet).forEach(tweet => {
const foundLinks = extractLinks(tweet);
foundLinks.forEach(linkData => {
if (links.has(linkData.url)) {
const existing = links.get(linkData.url);
existing.tweetCount++;
existing.tweets.push(linkData.tweetUrl);
} else {
links.set(linkData.url, {
url: linkData.url,
domain: linkData.domain,
tweetCount: 1,
tweets: [linkData.tweetUrl]
});
}
});
});
// Progress means new links OR new tweets scanned; tracking links alone
// would stop the scan early on stretches of tweets with no links
if (links.size === lastLinkCount && seenTweets.size === lastTweetCount) {
retries++;
} else {
retries = 0;
lastLinkCount = links.size;
lastTweetCount = seenTweets.size;
}
console.log(`📊 Found ${links.size} unique links from ${seenTweets.size} tweets...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log('');
console.log(`✅ Finished: ${links.size} links from ${seenTweets.size} tweets`);
console.log('');
// Group by domain
const byDomain = {};
links.forEach(link => {
if (!byDomain[link.domain]) {
byDomain[link.domain] = [];
}
byDomain[link.domain].push(link);
});
// Sort domains by link count
const sortedDomains = Object.entries(byDomain)
.sort((a, b) => b[1].length - a[1].length);
// Display summary
console.log('📊 LINKS BY DOMAIN:');
console.log('');
sortedDomains.slice(0, 15).forEach(([domain, domainLinks]) => {
console.log(` ${domain}: ${domainLinks.length} links`);
});
if (sortedDomains.length > 15) {
console.log(` ... and ${sortedDomains.length - 15} more domains`);
}
console.log('');
// Build result
const result = {
username,
scrapedAt: new Date().toISOString(),
totalLinks: links.size,
totalTweets: seenTweets.size,
byDomain: Object.fromEntries(sortedDomains),
allLinks: [...links.values()].sort((a, b) => b.tweetCount - a.tweetCount)
};
// Download
if (CONFIG.autoDownload) {
// JSON
const jsonBlob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const jsonUrl = URL.createObjectURL(jsonBlob);
const jsonLink = document.createElement('a');
jsonLink.href = jsonUrl;
jsonLink.download = `${username}_links_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(jsonLink);
jsonLink.click();
document.body.removeChild(jsonLink);
URL.revokeObjectURL(jsonUrl);
console.log('💾 JSON downloaded!');
// CSV (quote every field: URLs can contain commas and quotes)
const csvEscape = (v) => `"${String(v).replace(/"/g, '""')}"`;
const headers = ['URL', 'Domain', 'Times Shared', 'First Tweet'];
const rows = result.allLinks.map(l => [
l.url,
l.domain,
l.tweetCount,
l.tweets[0]
].map(csvEscape).join(','));
const csv = [headers.join(','), ...rows].join('\n');
const csvBlob = new Blob([csv], { type: 'text/csv' });
const csvUrl = URL.createObjectURL(csvBlob);
const csvLink = document.createElement('a');
csvLink.href = csvUrl;
csvLink.download = `${username}_links_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(csvLink);
csvLink.click();
document.body.removeChild(csvLink);
URL.revokeObjectURL(csvUrl);
console.log('💾 CSV downloaded!');
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ LINK SCRAPER COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`🔗 Total unique links: ${links.size}`);
console.log(`📊 Total domains: ${sortedDomains.length}`);
console.log('');
window.scrapedLinks = result;
console.log('💡 Access via: window.scrapedLinks');
return result;
})();
});
register("list-manager", function(){
(async function listManager() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// What to do: 'create' | 'rename' | 'delete'
action: 'create',
// For 'create': the new list name. For 'rename'/'delete': the EXISTING list name to target.
name: 'My New List',
// For 'rename' only: the name to change it to
newName: '',
// For 'create' only: optional description and privacy
description: 'Created with XActions',
isPrivate: false,
// Safety: preview the steps without changing anything. Set to false to execute.
dryRun: true,
// Delay between UI steps (ms)
stepDelay: 1500
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
createButton: '[data-testid="createList"], a[href="/i/lists/create"]',
nameInput: '[data-testid="listNameInput"], input[name="name"]',
descInput: '[data-testid="listDescriptionInput"], textarea[name="description"]',
saveButton: '[data-testid="listSaveButton"], [data-testid="listCreationSheet-headerButton"]',
listLink: 'a[href*="/lists/"]',
editButton: '[data-testid="editList"], [aria-label="Edit List"], [aria-label="Edit list"]',
deleteButton: '[data-testid="listDeleteButton"]',
confirm: '[data-testid="confirmationSheetConfirm"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
step: (msg) => console.log(` → ${msg}`)
};
let stopped = false;
window.stopListManager = () => {
stopped = true;
log.warning('Stop requested. The script will halt at the next step.');
};
const qs = (sel) => document.querySelector(sel);
// Wait for an element to appear (up to timeoutMs)
const waitFor = async (sel, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
if (stopped) return null;
const el = qs(sel);
if (el) return el;
await sleep(200);
}
return null;
};
// Set a value on a React-controlled input/textarea so the framework registers it
const setReactInput = (el, value) => {
const proto = el.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
// Find a clickable button whose visible text matches one of the labels
const findButtonByText = (labels) => {
const wanted = labels.map(l => l.toLowerCase());
const buttons = document.querySelectorAll('[role="button"], button, [role="menuitem"]');
for (const b of buttons) {
const t = (b.textContent || '').trim().toLowerCase();
if (wanted.some(w => t === w || t.startsWith(w))) return b;
}
return null;
};
// Locate a list row on the Lists page by its visible name
const findListLinkByName = (name) => {
const target = name.trim().toLowerCase();
for (const link of document.querySelectorAll(SELECTORS.listLink)) {
const text = (link.textContent || '').trim().toLowerCase();
if (text.includes(target)) return link;
}
return null;
};
// ============================================
// 🎯 ACTIONS
// ============================================
const doCreate = async () => {
log.info(`Create list: "${CONFIG.name}" (${CONFIG.isPrivate ? 'private' : 'public'})`);
if (!CONFIG.name.trim()) { log.error('CONFIG.name is empty. Set a name to create.'); return false; }
if (CONFIG.dryRun) {
log.step('DRY RUN: would open the create-list dialog');
log.step(`DRY RUN: would type name "${CONFIG.name}" and description "${CONFIG.description}"`);
log.step('DRY RUN: would save. Set CONFIG.dryRun = false to execute.');
return true;
}
const createBtn = await waitFor(SELECTORS.createButton, 4000);
if (!createBtn) { log.error('Could not find the "Create a new List" button. Are you on x.com/<you>/lists?'); return false; }
createBtn.click();
await sleep(CONFIG.stepDelay);
const nameInput = await waitFor(SELECTORS.nameInput);
if (!nameInput) { log.error('List name input did not appear.'); return false; }
nameInput.focus();
setReactInput(nameInput, CONFIG.name);
await sleep(500);
if (CONFIG.description) {
const descInput = qs(SELECTORS.descInput);
if (descInput) {
descInput.focus();
setReactInput(descInput, CONFIG.description);
await sleep(500);
}
}
// First "Next" advances past the details step in the current flow
let next = findButtonByText(['next']);
if (next && !next.getAttribute('aria-disabled')) {
next.click();
await sleep(CONFIG.stepDelay);
}
const saveBtn = qs(SELECTORS.saveButton) || findButtonByText(['done', 'create', 'save']);
if (!saveBtn) { log.error('Could not find the save/create button.'); return false; }
saveBtn.click();
await sleep(CONFIG.stepDelay);
log.success(`List "${CONFIG.name}" created.`);
return true;
};
const openTargetList = async () => {
const link = findListLinkByName(CONFIG.name);
if (!link) {
log.error(`Could not find a list named "${CONFIG.name}" on this page.`);
log.info('Make sure you are on x.com/<you>/lists and the list name matches.');
return false;
}
log.step(`Opening list "${CONFIG.name}"`);
link.click();
await sleep(CONFIG.stepDelay);
return true;
};
const doRename = async () => {
log.info(`Rename list: "${CONFIG.name}" -> "${CONFIG.newName}"`);
if (!CONFIG.newName.trim()) { log.error('CONFIG.newName is empty. Set the new name.'); return false; }
if (CONFIG.dryRun) {
log.step(`DRY RUN: would open "${CONFIG.name}", edit it, set name to "${CONFIG.newName}", and save.`);
log.step('Set CONFIG.dryRun = false to execute.');
return true;
}
if (!await openTargetList()) return false;
const editBtn = await waitFor(SELECTORS.editButton, 5000) || findButtonByText(['edit list']);
if (!editBtn) { log.error('Could not find the "Edit List" control.'); return false; }
editBtn.click();
await sleep(CONFIG.stepDelay);
const nameInput = await waitFor(SELECTORS.nameInput);
if (!nameInput) { log.error('List name input did not appear in the edit dialog.'); return false; }
nameInput.focus();
setReactInput(nameInput, CONFIG.newName);
await sleep(500);
const saveBtn = qs(SELECTORS.saveButton) || findButtonByText(['done', 'save']);
if (!saveBtn) { log.error('Could not find the save button.'); return false; }
saveBtn.click();
await sleep(CONFIG.stepDelay);
log.success(`List renamed to "${CONFIG.newName}".`);
return true;
};
const doDelete = async () => {
log.info(`Delete list: "${CONFIG.name}"`);
if (CONFIG.dryRun) {
log.step(`DRY RUN: would open "${CONFIG.name}", edit it, click Delete, and confirm.`);
log.step('Set CONFIG.dryRun = false to execute. This cannot be undone.');
return true;
}
if (!await openTargetList()) return false;
const editBtn = await waitFor(SELECTORS.editButton, 5000) || findButtonByText(['edit list']);
if (!editBtn) { log.error('Could not find the "Edit List" control.'); return false; }
editBtn.click();
await sleep(CONFIG.stepDelay);
const deleteBtn = await waitFor(SELECTORS.deleteButton, 4000) || findButtonByText(['delete list']);
if (!deleteBtn) { log.error('Could not find the "Delete List" button.'); return false; }
deleteBtn.click();
await sleep(CONFIG.stepDelay);
const confirm = await waitFor(SELECTORS.confirm, 4000) || findButtonByText(['delete']);
if (!confirm) { log.error('Could not find the delete confirmation button.'); return false; }
confirm.click();
await sleep(CONFIG.stepDelay);
log.success(`List "${CONFIG.name}" deleted.`);
return true;
};
// ============================================
// 🚀 RUN
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🗂️ LIST MANAGER - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn (do not hard-redirect) if not on a lists page
if (!/\/lists/.test(window.location.pathname) && !/\/i\/lists/.test(window.location.pathname)) {
log.warning('You are not on your Lists page.');
log.info('Go to https://x.com/<your-handle>/lists, then paste this script again.');
return;
}
if (CONFIG.dryRun) {
log.warning('DRY RUN is ON. No changes will be made. Set CONFIG.dryRun = false to execute.');
}
log.info('To abort mid-flow: window.stopListManager()');
console.log('');
let ok = false;
try {
if (CONFIG.action === 'create') ok = await doCreate();
else if (CONFIG.action === 'rename') ok = await doRename();
else if (CONFIG.action === 'delete') ok = await doDelete();
else log.error(`Unknown action "${CONFIG.action}". Use 'create', 'rename', or 'delete'.`);
} catch (e) {
log.error(`Action failed: ${e.message}`);
}
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ 🏁 LIST MANAGER - DONE ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log(` Action: ${CONFIG.action}`);
console.log(` Target: ${CONFIG.name}`);
console.log(` Result: ${CONFIG.dryRun ? 'dry run (no change)' : (ok ? 'success' : 'not completed')}`);
console.log('✅ Script completed! by nichxbt');
return { action: CONFIG.action, dryRun: CONFIG.dryRun, success: ok };
})();
});
register("manage-muted-words", function(){
(async function manageMutedWords() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// 'add' -> mute every phrase in `words`
// 'remove' -> unmute every phrase in `words`
// 'list' -> scrape the words you already mute (console + JSON download)
action: 'list',
// The words/phrases to add or remove. Ignored for 'list'.
words: [
'giveaway',
'airdrop scam'
],
// Options applied to each word when adding (X's mute settings).
muteOptions: {
// 'anyone' (from anyone) or 'follow' (from people you don't follow).
from: 'anyone',
// Mute in the Home timeline.
home: true,
// Mute in Notifications.
notifications: true,
// 'forever', '24h', '7d', or '30d'.
duration: 'forever'
},
// Minimum delay between actions (ms).
minDelay: 1500,
// Maximum delay between actions (ms).
maxDelay: 4000
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
// Entry points into the "Add muted word" flow.
addLink: 'a[href*="add_muted_keyword"], [data-testid="addMutedWord"]',
keywordInput: 'input[name="keyword"], [data-testid="mutedKeywordTextInput"]',
saveButton: '[data-testid="settingsDetailSave"], [data-testid="settingsSave"], [data-testid="Profile_Save_Button"]',
unmuteButton: '[data-testid="unmuteKeyword"], [data-testid="deleteMutedKeyword"]',
confirm: '[data-testid="confirmationSheetConfirm"]',
// Each muted word renders in a settings row.
row: '[data-testid="cellInnerDiv"], [data-testid="MutedKeyword"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => sleep(
Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay
);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
// Poll for an element to appear (returns element or null).
const waitFor = async (selector, tries = 8, gap = 500) => {
for (let i = 0; i < tries; i++) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(gap);
}
return null;
};
// Set an input value through React's native setter so the change registers.
const setNativeValue = (el, value) => {
const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
if (desc && desc.set) desc.set.call(el, value);
else el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
};
// Click a radio/checkbox/switch whose visible label matches any of `needles`.
const clickOptionByText = (needles) => {
const wants = needles.map(n => n.toLowerCase());
const controls = document.querySelectorAll('[role="radio"], [role="checkbox"], [role="switch"], label');
for (const ctrl of controls) {
const txt = (ctrl.textContent || '').trim().toLowerCase();
if (txt && wants.some(w => txt.includes(w))) {
ctrl.click();
return true;
}
}
return false;
};
const download = (data, filename) => {
try {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
log.success(`Downloaded ${filename}`);
} catch (e) {
log.warning(`Could not trigger download: ${e.message}`);
}
};
// Read the muted words currently rendered in the list.
const scrapeMutedWords = () => {
const words = new Set();
// Muted words each link to their own edit page: /settings/muted_keywords/<id>.
document.querySelectorAll('a[href*="/settings/muted_keywords/"]').forEach(a => {
const text = (a.textContent || '').trim();
if (text) words.add(text);
});
// Fallback: settings rows on the muted keywords page.
if (words.size === 0) {
document.querySelectorAll(SELECTORS.row).forEach(row => {
const span = row.querySelector('span');
const text = (span?.textContent || '').trim();
if (text && text.length <= 100) words.add(text);
});
}
return Array.from(words);
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { added: 0, removed: 0, listed: 0, skipped: 0, errors: 0 };
// Stop switch: run window.stopManageMutedWords() to abort after the current word.
let stopped = false;
window.stopManageMutedWords = () => {
stopped = true;
log.warning('Stop requested. Finishing the current word, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔇 MANAGE MUTED WORDS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn, do not hard-redirect.
if (!/muted_keywords/.test(window.location.pathname)) {
log.warning('Go to x.com/settings/muted_keywords, then run this again.');
return stats;
}
const action = String(CONFIG.action || '').toLowerCase();
// ---- LIST ----
if (action === 'list') {
const words = scrapeMutedWords();
stats.listed = words.length;
if (words.length === 0) {
log.info('No muted words found on this page. If you have some, scroll the list once so they render, then re-run.');
} else {
log.success(`You currently mute ${words.length} word(s)/phrase(s):`);
words.forEach((w, i) => console.log(` ${i + 1}. "${w}"`));
download(
{ scrapedAt: new Date().toISOString(), count: words.length, mutedWords: words },
`xactions-muted-words-${new Date().toISOString().slice(0, 10)}.json`
);
}
console.log(`\n✅ Listed ${stats.listed} muted word(s). by nichxbt`);
return stats;
}
// ---- ADD / REMOVE guardrails ----
if (action !== 'add' && action !== 'remove') {
log.error(`Unknown action "${CONFIG.action}". Use 'add', 'remove', or 'list'.`);
return stats;
}
const words = Array.isArray(CONFIG.words) ? CONFIG.words.map(w => String(w).trim()).filter(Boolean) : [];
if (words.length === 0) {
log.warning('CONFIG.words is empty. Add the phrases you want to ' + action + '.');
return stats;
}
log.info(`Action: ${action} | ${words.length} word(s)`);
log.info('To stop early: window.stopManageMutedWords()');
// ---- ADD ----
const addWord = async (word) => {
const addLink = await waitFor(SELECTORS.addLink, 6, 400);
if (!addLink) throw new Error('Add control not found');
addLink.click();
const input = await waitFor(SELECTORS.keywordInput, 10, 400);
if (!input) throw new Error('Keyword input did not appear');
input.focus();
setNativeValue(input, word);
await sleep(400);
// Apply mute options (best-effort; X wording varies by build).
const o = CONFIG.muteOptions || {};
if (o.from === 'follow') clickOptionByText(["people you don't follow", 'you don’t follow', "don't follow"]);
else clickOptionByText(['from anyone', 'anyone']);
if (o.duration && o.duration !== 'forever') {
const map = { '24h': ['24 hours', '24'], '7d': ['7 days', '7'], '30d': ['30 days', '30'] };
clickOptionByText(map[o.duration] || ['forever']);
}
await sleep(300);
const saveBtn = await waitFor(SELECTORS.saveButton, 6, 400);
if (!saveBtn) throw new Error('Save button not found');
saveBtn.click();
await sleep(1200);
};
// ---- REMOVE ----
const removeWord = async (word) => {
const target = word.toLowerCase();
// Find the row/link for this exact word.
const links = Array.from(document.querySelectorAll('a[href*="/settings/muted_keywords/"]'));
let entry = links.find(a => (a.textContent || '').trim().toLowerCase() === target);
if (!entry) {
// Fallback to a settings row containing the text.
entry = Array.from(document.querySelectorAll(SELECTORS.row))
.find(r => (r.textContent || '').trim().toLowerCase().includes(target));
}
if (!entry) return false;
entry.click();
const unmute = await waitFor(SELECTORS.unmuteButton, 8, 400);
if (!unmute) {
// Some builds show unmute as a button labelled "Unmute word".
const byText = Array.from(document.querySelectorAll('[role="button"], button'))
.find(b => (b.textContent || '').trim().toLowerCase().includes('unmute'));
if (!byText) throw new Error('Unmute control not found');
byText.click();
} else {
unmute.click();
}
const confirm = await waitFor(SELECTORS.confirm, 4, 400);
if (confirm) confirm.click();
await sleep(1000);
return true;
};
for (const word of words) {
if (stopped) break;
try {
if (action === 'add') {
await addWord(word);
stats.added++;
log.success(`Muted: "${word}" (${stats.added}/${words.length})`);
} else {
const ok = await removeWord(word);
if (ok) {
stats.removed++;
log.success(`Unmuted: "${word}" (${stats.removed} removed)`);
} else {
stats.skipped++;
log.warning(`Not in your muted list: "${word}". Skipped.`);
}
}
} catch (error) {
stats.errors++;
log.error(`Failed to ${action} "${word}": ${error.message}`);
}
if (!stopped) await randomDelay();
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 MANAGE MUTED WORDS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Added: ${String(stats.added).padEnd(32)}║
║ 🗑️ Removed: ${String(stats.removed).padEnd(32)}║
║ ⏭️ Skipped: ${String(stats.skipped).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("mass-block", function(){
var CONFIG = {
// List of usernames to block (without @)
usersToBlock: [
// 'username1',
// 'username2',
// Add usernames here
],
// Delay between blocks (ms) - don't go below 2000 to avoid rate limits
blockDelay: 3000,
// Dry run mode - set to false to actually block
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function massBlock() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Navigate within the SPA. Assigning window.location.href triggers a full
// page load, which destroys this console script after the first user.
const spaNavigate = (url) => {
try {
const target = new URL(url, window.location.href);
if (target.origin === window.location.origin) {
window.history.pushState({}, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
return;
}
} catch (e) {}
window.location.href = url;
};
// Stop switch: run window.stopMassBlock() from the console to abort after
// the account currently being processed.
let stopped = false;
window.stopMassBlock = () => {
stopped = true;
console.log('🛑 Stop requested. Finishing the current account, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🚫 XActions — Mass Block ║
║ Block multiple users from a list ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be blocked ║' : '║ 🔴 LIVE MODE - Accounts WILL be blocked ║'}
╚══════════════════════════════════════════════════════════════╝
`);
console.log('💡 To stop early: window.stopMassBlock()\n');
if (CONFIG.usersToBlock.length === 0) {
console.log('❌ No users to block! Edit CONFIG.usersToBlock and add usernames.');
console.log('\nExample:');
console.log(' usersToBlock: [');
console.log(' "spammer1",');
console.log(' "spammer2",');
console.log(' ],');
return;
}
console.log(`📋 Users to block: ${CONFIG.usersToBlock.length}`);
CONFIG.usersToBlock.forEach((u, i) => {
console.log(` ${i + 1}. @${u}`);
});
console.log('');
if (CONFIG.dryRun) {
console.log('⚠️ DRY RUN MODE - Set CONFIG.dryRun = false to actually block\n');
return;
}
const results = {
blocked: [],
failed: [],
notFound: []
};
for (const username of CONFIG.usersToBlock) {
if (stopped) {
console.log('🛑 Stopped by user.');
break;
}
console.log(`\n⏳ Processing @${username}...`);
try {
// Navigate to user's profile (SPA navigation keeps this script alive)
const profileUrl = `https://x.com/${username}`;
spaNavigate(profileUrl);
await sleep(3000); // Wait for page load
// Find the more button (poll briefly: profiles can render slowly)
let moreButton = document.querySelector('[data-testid="userActions"]');
let waited = 0;
while (!moreButton && waited < 5000) {
await sleep(500);
waited += 500;
moreButton = document.querySelector('[data-testid="userActions"]');
}
if (!moreButton) {
console.log(` ❌ Could not find user menu for @${username}`);
results.notFound.push(username);
continue;
}
moreButton.click();
await sleep(500);
// Find block option
const blockOption = document.querySelector('[data-testid="block"]');
if (!blockOption) {
console.log(` ❌ Block option not found for @${username}`);
results.failed.push(username);
document.body.click(); // Close menu
continue;
}
blockOption.click();
await sleep(500);
// Confirm block
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
confirmBtn.click();
console.log(` ✅ Blocked @${username}`);
results.blocked.push(username);
} else {
console.log(` ❌ Could not confirm block for @${username}`);
results.failed.push(username);
}
await sleep(CONFIG.blockDelay);
} catch (error) {
console.log(` ❌ Error blocking @${username}: ${error.message}`);
results.failed.push(username);
}
}
console.log('\n' + '═'.repeat(60));
console.log('📊 RESULTS');
console.log('═'.repeat(60));
console.log(`\n✅ Blocked: ${results.blocked.length}`);
results.blocked.forEach(u => console.log(` @${u}`));
if (results.failed.length > 0) {
console.log(`\n❌ Failed: ${results.failed.length}`);
results.failed.forEach(u => console.log(` @${u}`));
}
if (results.notFound.length > 0) {
console.log(`\n❓ Not found: ${results.notFound.length}`);
results.notFound.forEach(u => console.log(` @${u}`));
}
// Save log
const storageKey = 'xactions_mass_block_log';
const log = {
timestamp: new Date().toISOString(),
...results
};
const existing = JSON.parse(localStorage.getItem(storageKey) || '[]');
existing.push(log);
localStorage.setItem(storageKey, JSON.stringify(existing.slice(-50)));
console.log('\n💾 Results saved to localStorage');
})();
});
register("mass-unblock", function(){
var CONFIG = {
// Unblock all blocked accounts (true) or just specific ones (false)
unblockAll: true,
// If unblockAll is false, specify usernames to unblock
usersToUnblock: [
// 'username1',
// 'username2',
],
// Delay between unblocks (ms)
unblockDelay: 1500,
// Max accounts to unblock per run
maxUnblocks: 100,
// Scroll settings
scrollDelay: 1500,
maxScrolls: 20,
// Dry run mode
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function massUnblock() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Stop switch: run window.stopMassUnblock() from the console to abort
// after the account currently being processed.
let stopped = false;
window.stopMassUnblock = () => {
stopped = true;
console.log('🛑 Stop requested. Finishing the current account, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ ✅ XActions — Mass Unblock ║
║ Unblock multiple users ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be unblocked ║' : '║ 🔴 LIVE MODE - Accounts WILL be unblocked ║'}
╚══════════════════════════════════════════════════════════════╝
`);
console.log('💡 To stop early: window.stopMassUnblock()\n');
// Check if on blocked accounts page
if (!window.location.href.includes('/settings/blocked')) {
console.error('❌ Please navigate to your blocked accounts page first!');
console.log('👉 Go to: https://x.com/settings/blocked/all');
return;
}
const $userCell = '[data-testid="UserCell"]';
const $unblockBtn = '[data-testid$="-unblock"]';
console.log('🔍 Scanning blocked accounts...\n');
const blockedUsers = new Map();
let scrollCount = 0;
let retries = 0;
while (scrollCount < CONFIG.maxScrolls && retries < 3) {
const prevSize = blockedUsers.size;
document.querySelectorAll($userCell).forEach(cell => {
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!username || blockedUsers.has(username)) return;
const unblockBtn = cell.querySelector($unblockBtn);
if (unblockBtn) {
blockedUsers.set(username, { element: cell, button: unblockBtn });
}
});
if (blockedUsers.size === prevSize) retries++;
else retries = 0;
console.log(` Found ${blockedUsers.size} blocked accounts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Found ${blockedUsers.size} blocked accounts\n`);
if (blockedUsers.size === 0) {
console.log('🎉 No blocked accounts found!');
return;
}
// Determine which users to unblock
let toUnblock = [];
if (CONFIG.unblockAll) {
toUnblock = Array.from(blockedUsers.entries()).slice(0, CONFIG.maxUnblocks);
console.log(`📋 Will unblock ${toUnblock.length} accounts (all)`);
} else {
toUnblock = CONFIG.usersToUnblock
.filter(u => blockedUsers.has(u))
.map(u => [u, blockedUsers.get(u)]);
console.log(`📋 Will unblock ${toUnblock.length} specified accounts`);
const notFound = CONFIG.usersToUnblock.filter(u => !blockedUsers.has(u));
if (notFound.length > 0) {
console.log(`⚠️ Not found in blocked list: ${notFound.join(', ')}`);
}
}
if (toUnblock.length === 0) {
console.log('❌ No accounts to unblock!');
return;
}
console.log('');
toUnblock.slice(0, 20).forEach(([username], i) => {
console.log(` ${i + 1}. @${username}`);
});
if (toUnblock.length > 20) {
console.log(` ... and ${toUnblock.length - 20} more`);
}
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE - No unblocks performed');
console.log('Set CONFIG.dryRun = false to actually unblock');
console.log('═'.repeat(60));
return;
}
console.log('\n' + '═'.repeat(60));
console.log('🔄 UNBLOCKING ACCOUNTS');
console.log('═'.repeat(60));
let unblocked = 0;
const unblockedUsers = [];
// Unblocking a row on this page removes it from the (virtualized) list,
// so cell/button references captured during the initial scan can go
// stale after the first successful unblock. Re-resolve a live button for
// the username before clicking, falling back to the cached reference.
const findUnblockButton = (username) => {
for (const cell of document.querySelectorAll($userCell)) {
const link = cell.querySelector('a[href^="/"]');
const cellUsername = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (cellUsername === username) {
return { element: cell, button: cell.querySelector($unblockBtn) };
}
}
return null;
};
for (const [username, data] of toUnblock) {
if (stopped) {
console.log('🛑 Stopped by user.');
break;
}
try {
const live = findUnblockButton(username);
const target = (live && live.button) ? live : data;
if (!document.body.contains(target.button)) {
console.log(` ❌ @${username} is no longer in the list (already unblocked or off-screen). Skipping.`);
continue;
}
target.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(300);
target.button.click();
await sleep(500);
// Check for confirmation dialog
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
confirmBtn.click();
await sleep(300);
}
unblocked++;
unblockedUsers.push(username);
console.log(`✅ Unblocked @${username}`);
await sleep(CONFIG.unblockDelay);
} catch (e) {
console.log(`❌ Failed to unblock @${username}`);
}
}
console.log('\n' + '═'.repeat(60));
console.log(`✅ Unblocked ${unblocked}/${toUnblock.length} accounts`);
console.log('═'.repeat(60));
// Save log
const storageKey = 'xactions_unblock_log';
const log = {
timestamp: new Date().toISOString(),
// Record the accounts actually unblocked; slicing the candidate list
// logs the wrong names whenever an unblock in the middle failed
unblocked: unblockedUsers
};
const existing = JSON.parse(localStorage.getItem(storageKey) || '[]');
existing.push(log);
localStorage.setItem(storageKey, JSON.stringify(existing.slice(-50)));
console.log('\n💾 Results saved to localStorage');
})();
});
register("mass-unmute", function(){
var CONFIG = {
// Unmute all muted accounts
unmuteAll: true,
// If unmuteAll is false, specify usernames to unmute
usersToUnmute: [
// 'username1',
// 'username2',
],
// Delay between unmutes (ms)
unmuteDelay: 1000,
// Max accounts to unmute per run
maxUnmutes: 200,
// Scroll settings
scrollDelay: 1500,
maxScrolls: 30,
// Dry run mode
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function massUnmute() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Stop switch: run window.stopMassUnmute() from the console to abort
// after the account currently being processed.
let stopped = false;
window.stopMassUnmute = () => {
stopped = true;
console.log('🛑 Stop requested. Finishing the current account, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🔊 XActions — Mass Unmute ║
║ Unmute multiple users ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be unmuted ║' : '║ 🔴 LIVE MODE - Accounts WILL be unmuted ║'}
╚══════════════════════════════════════════════════════════════╝
`);
console.log('💡 To stop early: window.stopMassUnmute()\n');
// Check if on muted accounts page
if (!window.location.href.includes('/settings/muted')) {
console.error('❌ Please navigate to your muted accounts page first!');
console.log('👉 Go to: https://x.com/settings/muted/all');
return;
}
const $userCell = '[data-testid="UserCell"]';
console.log('🔍 Scanning muted accounts...\n');
const mutedUsers = new Map();
let scrollCount = 0;
let retries = 0;
while (scrollCount < CONFIG.maxScrolls && retries < 3) {
const prevSize = mutedUsers.size;
document.querySelectorAll($userCell).forEach(cell => {
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!username || mutedUsers.has(username)) return;
mutedUsers.set(username, { element: cell });
});
if (mutedUsers.size === prevSize) retries++;
else retries = 0;
console.log(` Found ${mutedUsers.size} muted accounts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Found ${mutedUsers.size} muted accounts\n`);
if (mutedUsers.size === 0) {
console.log('🎉 No muted accounts found!');
return;
}
// Determine which users to unmute
let toUnmute = [];
if (CONFIG.unmuteAll) {
toUnmute = Array.from(mutedUsers.entries()).slice(0, CONFIG.maxUnmutes);
console.log(`📋 Will unmute ${toUnmute.length} accounts (all)`);
} else {
toUnmute = CONFIG.usersToUnmute
.filter(u => mutedUsers.has(u))
.map(u => [u, mutedUsers.get(u)]);
console.log(`📋 Will unmute ${toUnmute.length} specified accounts`);
}
if (toUnmute.length === 0) {
console.log('❌ No accounts to unmute!');
return;
}
toUnmute.slice(0, 20).forEach(([username], i) => {
console.log(` ${i + 1}. @${username}`);
});
if (toUnmute.length > 20) {
console.log(` ... and ${toUnmute.length - 20} more`);
}
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE - No unmutes performed');
console.log('Set CONFIG.dryRun = false to actually unmute');
console.log('═'.repeat(60));
return;
}
console.log('\n' + '═'.repeat(60));
console.log('🔊 UNMUTING ACCOUNTS');
console.log('═'.repeat(60));
let unmuted = 0;
const unmutedUsers = [];
// Unmuting a row on this page removes it from the (virtualized) muted
// list, so a cell reference captured during the initial scan can go stale
// after the first successful unmute. Re-resolve the live cell for the
// username before interacting with it, falling back to the cached one.
const findUserCell = (username) => {
for (const cell of document.querySelectorAll($userCell)) {
const link = cell.querySelector('a[href^="/"]');
const cellUsername = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (cellUsername === username) return cell;
}
return null;
};
for (const [username, data] of toUnmute) {
if (stopped) {
console.log('🛑 Stopped by user.');
break;
}
try {
const element = findUserCell(username) || data.element;
if (!document.body.contains(element)) {
console.log(`❌ @${username} is no longer in the list (already unmuted or off-screen). Skipping.`);
continue;
}
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(300);
// Find unmute button in the cell
const unmuteBtn = element.querySelector('[data-testid$="-unmute"], button[aria-label*="Unmute"]');
if (unmuteBtn) {
unmuteBtn.click();
await sleep(300);
unmuted++;
unmutedUsers.push(username);
console.log(`✅ Unmuted @${username}`);
} else {
// Try via the more menu
const moreBtn = element.querySelector('[data-testid="userActions"]');
if (moreBtn) {
moreBtn.click();
await sleep(300);
const unmuteOption = document.querySelector('[data-testid="unmute"]');
if (unmuteOption) {
unmuteOption.click();
unmuted++;
unmutedUsers.push(username);
console.log(`✅ Unmuted @${username}`);
}
document.body.click(); // Close menu
}
}
await sleep(CONFIG.unmuteDelay);
} catch (e) {
console.log(`❌ Failed to unmute @${username}`);
}
}
console.log('\n' + '═'.repeat(60));
console.log(`✅ Unmuted ${unmuted}/${toUnmute.length} accounts`);
console.log('═'.repeat(60));
// Save log
const storageKey = 'xactions_unmute_log';
const log = {
timestamp: new Date().toISOString(),
// Record the accounts actually unmuted; slicing the candidate list
// logs the wrong names whenever an unmute in the middle failed
unmuted: unmutedUsers
};
const existing = JSON.parse(localStorage.getItem(storageKey) || '[]');
existing.push(log);
localStorage.setItem(storageKey, JSON.stringify(existing.slice(-50)));
console.log('\n💾 Results saved to localStorage');
})();
});
register("monitor-account", function(){
var CONFIG = {
// Delay between scrolls
scrollDelay: 2000,
// Maximum scroll attempts
maxScrolls: 100,
// Retry when no new users found
maxRetries: 5,
// Auto-download report
autoDownload: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function monitorAccount() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $userCell = '[data-testid="UserCell"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 👁️ MONITOR ACCOUNT ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Detect page type and user
const url = window.location.href;
const pathMatch = url.match(/x\.com\/([^\/]+)\/(followers|following)/);
if (!pathMatch) {
console.error('❌ ERROR: Must be on a followers or following page!');
console.log('📍 Example: https://x.com/elonmusk/followers');
return;
}
const username = pathMatch[1];
const pageType = pathMatch[2]; // 'followers' or 'following'
const storageKey = `xactions_monitor_${username}_${pageType}`;
console.log(`👤 Monitoring: @${username}`);
console.log(`📋 Page type: ${pageType}`);
console.log('');
console.log('🚀 Scraping current list...');
/**
* Extract username from user cell
*/
function getUsername(cell) {
const link = cell.querySelector('a[href^="/"]');
if (link) {
const href = link.getAttribute('href');
return href ? href.replace('/', '').split('/')[0] : null;
}
return null;
}
function getDisplayName(cell) {
const span = cell.querySelector('[dir="ltr"] span');
return span ? span.textContent : null;
}
// Scrape users
const currentUsers = new Map();
let lastCount = 0;
let retries = 0;
let scrolls = 0;
while (scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const cells = document.querySelectorAll($userCell);
cells.forEach(cell => {
const user = getUsername(cell);
const name = getDisplayName(cell);
if (user && !currentUsers.has(user)) {
currentUsers.set(user, name || user);
}
});
if (currentUsers.size === lastCount) {
retries++;
} else {
retries = 0;
lastCount = currentUsers.size;
}
console.log(`📊 Scraped ${currentUsers.size} accounts...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log(`✅ Finished: ${currentUsers.size} accounts`);
console.log('');
// Load previous
let previous = null;
try {
const saved = localStorage.getItem(storageKey);
if (saved) previous = JSON.parse(saved);
} catch (e) {}
const timestamp = new Date().toISOString();
// Save current
const snapshot = {
savedAt: timestamp,
username,
pageType,
count: currentUsers.size,
users: Object.fromEntries(currentUsers)
};
localStorage.setItem(storageKey, JSON.stringify(snapshot));
console.log('💾 Snapshot saved');
if (!previous) {
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📸 FIRST SNAPSHOT SAVED! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 @${username}'s ${pageType}: ${currentUsers.size}`);
console.log('');
console.log('💡 Run again later to detect changes!');
window.accountSnapshot = snapshot;
return snapshot;
}
// Compare
console.log('🔄 Comparing with previous snapshot...');
console.log(` Previous: ${previous.count} (${previous.savedAt})`);
console.log(` Current: ${currentUsers.size}`);
console.log('');
const prevSet = new Set(Object.keys(previous.users));
const currSet = new Set(currentUsers.keys());
// Removed
const removed = [];
prevSet.forEach(u => {
if (!currSet.has(u)) {
removed.push({ username: u, displayName: previous.users[u] });
}
});
// Added
const added = [];
currSet.forEach(u => {
if (!prevSet.has(u)) {
added.push({ username: u, displayName: currentUsers.get(u) });
}
});
// Results
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📊 CHANGES DETECTED ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
if (removed.length > 0) {
const label = pageType === 'followers' ? 'UNFOLLOWED' : 'STOPPED FOLLOWING';
console.log(`🚫 ${label} (${removed.length}):`);
removed.forEach((u, i) => {
console.log(` ${i + 1}. @${u.username}`);
});
console.log('');
}
if (added.length > 0) {
const label = pageType === 'followers' ? 'NEW FOLLOWERS' : 'NOW FOLLOWING';
console.log(`🆕 ${label} (${added.length}):`);
added.forEach((u, i) => {
console.log(` ${i + 1}. @${u.username}`);
});
console.log('');
}
if (removed.length === 0 && added.length === 0) {
console.log('✅ No changes detected!');
}
// Download report
if (CONFIG.autoDownload && (removed.length > 0 || added.length > 0)) {
let report = `ACCOUNT MONITOR REPORT\n`;
report += `${'='.repeat(50)}\n`;
report += `Account: @${username}\n`;
report += `Type: ${pageType}\n`;
report += `Generated: ${timestamp}\n`;
report += `Previous: ${previous.count} (${previous.savedAt})\n`;
report += `Current: ${currentUsers.size}\n`;
report += `${'='.repeat(50)}\n\n`;
if (removed.length > 0) {
report += `REMOVED (${removed.length}):\n`;
removed.forEach((u, i) => {
report += `${i + 1}. @${u.username}\n`;
});
report += '\n';
}
if (added.length > 0) {
report += `ADDED (${added.length}):\n`;
added.forEach((u, i) => {
report += `${i + 1}. @${u.username}\n`;
});
}
const blob = new Blob([report], { type: 'text/plain' });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `${username}_${pageType}_changes_${timestamp.split('T')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
console.log('💾 Report downloaded!');
}
const result = { username, pageType, removed, added, timestamp };
window.accountChanges = result;
console.log('💡 Access via: window.accountChanges');
return result;
})();
});
register("multi-account", function(){
var CONFIG = {
// Storage key prefix
storagePrefix: 'xactions_multi_',
// Auto-detect current logged-in account
autoDetect: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function multiAccountManager() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 👥 MULTI-ACCOUNT MANAGER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Storage keys
const KEYS = {
accounts: CONFIG.storagePrefix + 'accounts',
current: CONFIG.storagePrefix + 'current',
stats: CONFIG.storagePrefix + 'stats',
schedule: CONFIG.storagePrefix + 'schedule',
};
// Storage helpers
const storage = {
get: (key) => {
try { return JSON.parse(localStorage.getItem(key) || 'null'); }
catch { return null; }
},
set: (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
}
};
// Get current username from page
const getCurrentUsername = () => {
const accountBtn = document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]');
if (accountBtn) {
// The button holds both the display name and the @handle; the first
// span is the display name, so look for the span starting with "@"
const spans = accountBtn.querySelectorAll('span');
for (const span of spans) {
const text = span.textContent.trim();
if (text.startsWith('@')) {
return text.slice(1).toLowerCase();
}
}
}
return null;
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Accounts = {
// Get all accounts
getAll: () => storage.get(KEYS.accounts) || [],
// Add account (just stores the username for tracking)
add: (username, notes = '') => {
const accounts = window.XActions.Accounts.getAll();
const cleanUsername = username.replace('@', '').toLowerCase();
if (accounts.find(a => a.username === cleanUsername)) {
console.warn(`⚠️ Account @${cleanUsername} already exists.`);
return false;
}
accounts.push({
username: cleanUsername,
notes,
addedAt: Date.now(),
lastUsed: null,
status: 'active',
stats: {
follows: 0,
unfollows: 0,
likes: 0,
tweets: 0,
}
});
storage.set(KEYS.accounts, accounts);
console.log(`✅ Added account @${cleanUsername}`);
return true;
},
// Remove account
remove: (username) => {
let accounts = window.XActions.Accounts.getAll();
const cleanUsername = username.replace('@', '').toLowerCase();
const before = accounts.length;
accounts = accounts.filter(a => a.username !== cleanUsername);
if (accounts.length < before) {
storage.set(KEYS.accounts, accounts);
console.log(`✅ Removed account @${cleanUsername}`);
return true;
}
console.warn(`⚠️ Account @${cleanUsername} not found.`);
return false;
},
// Update account status
setStatus: (username, status) => {
const accounts = window.XActions.Accounts.getAll();
const cleanUsername = username.replace('@', '').toLowerCase();
const account = accounts.find(a => a.username === cleanUsername);
if (account) {
account.status = status;
storage.set(KEYS.accounts, accounts);
console.log(`✅ Updated @${cleanUsername} status to: ${status}`);
return true;
}
console.warn(`⚠️ Account @${cleanUsername} not found.`);
return false;
},
// Mark account as used
markUsed: (username) => {
const accounts = window.XActions.Accounts.getAll();
const cleanUsername = username?.replace('@', '').toLowerCase();
const account = accounts.find(a => a.username === cleanUsername);
if (account) {
account.lastUsed = Date.now();
storage.set(KEYS.accounts, accounts);
storage.set(KEYS.current, cleanUsername);
}
},
// Get current account
current: () => {
const detected = getCurrentUsername();
if (detected) {
window.XActions.Accounts.markUsed(detected);
}
return detected || storage.get(KEYS.current);
},
// Get next account to use (for rotation)
next: () => {
const accounts = window.XActions.Accounts.getAll().filter(a => a.status === 'active');
if (accounts.length === 0) {
console.warn('⚠️ No active accounts available.');
return null;
}
// Sort by lastUsed (null = never used = highest priority)
accounts.sort((a, b) => {
if (!a.lastUsed) return -1;
if (!b.lastUsed) return 1;
return a.lastUsed - b.lastUsed;
});
return accounts[0];
},
// Update stats for current account
updateStats: (statType, increment = 1) => {
const accounts = window.XActions.Accounts.getAll();
const current = getCurrentUsername();
const account = accounts.find(a => a.username === current);
if (account && account.stats[statType] !== undefined) {
account.stats[statType] += increment;
storage.set(KEYS.accounts, accounts);
}
},
// List all accounts
list: () => {
const accounts = window.XActions.Accounts.getAll();
const current = getCurrentUsername();
console.log('');
console.log('═'.repeat(60));
console.log('📋 ACCOUNT LIST');
console.log('═'.repeat(60));
if (accounts.length === 0) {
console.log('No accounts added yet.');
console.log('Use: XActions.Accounts.add("username")');
} else {
accounts.forEach((a, i) => {
const lastUsed = a.lastUsed ? new Date(a.lastUsed).toLocaleString() : 'Never';
const statusEmoji = {
active: '✅',
paused: '⏸️',
limited: '⚠️',
suspended: '🚫',
}[a.status] || '❓';
const isCurrent = a.username === current ? ' 👈 CURRENT' : '';
console.log(`${i + 1}. ${statusEmoji} @${a.username}${isCurrent}`);
console.log(` Last used: ${lastUsed}`);
console.log(` Stats: ${a.stats.follows}F / ${a.stats.likes}L / ${a.stats.tweets}T`);
if (a.notes) console.log(` Notes: ${a.notes}`);
});
}
console.log('═'.repeat(60));
console.log('');
},
// Show stats for all accounts
stats: () => {
const accounts = window.XActions.Accounts.getAll();
console.log('');
console.log('📊 ACCOUNT STATISTICS:');
console.log('');
let totalFollows = 0;
let totalLikes = 0;
let totalTweets = 0;
accounts.forEach(a => {
console.log(`@${a.username}:`);
console.log(` Follows: ${a.stats.follows}, Likes: ${a.stats.likes}, Tweets: ${a.stats.tweets}`);
totalFollows += a.stats.follows;
totalLikes += a.stats.likes;
totalTweets += a.stats.tweets;
});
console.log('');
console.log(`📈 TOTALS: ${totalFollows} follows, ${totalLikes} likes, ${totalTweets} tweets`);
console.log('');
},
// Switch account (opens account switcher)
switch: () => {
const switcherBtn = document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]');
if (switcherBtn) {
switcherBtn.click();
console.log('📋 Account switcher opened. Select an account.');
} else {
console.error('❌ Account switcher not found. Make sure you\'re on X.com');
}
},
// Clear all account data
clear: () => {
if (confirm('⚠️ This will delete ALL saved account data. Continue?')) {
localStorage.removeItem(KEYS.accounts);
localStorage.removeItem(KEYS.current);
localStorage.removeItem(KEYS.stats);
console.log('✅ All account data cleared.');
}
},
// Export accounts (without sensitive data)
export: () => {
const accounts = window.XActions.Accounts.getAll().map(a => ({
username: a.username,
notes: a.notes,
status: a.status,
stats: a.stats,
}));
const json = JSON.stringify(accounts, null, 2);
console.log('📋 Account data (copy this):');
console.log(json);
// Also copy to clipboard (only claim success when the write succeeds)
if (navigator.clipboard) {
navigator.clipboard.writeText(json)
.then(() => console.log('✅ Copied to clipboard!'))
.catch(() => console.warn('⚠️ Clipboard copy failed. Copy the JSON above manually.'));
}
},
// Import accounts
import: (jsonString) => {
try {
const imported = JSON.parse(jsonString);
const accounts = window.XActions.Accounts.getAll();
imported.forEach(a => {
if (!accounts.find(existing => existing.username === a.username)) {
accounts.push({
...a,
addedAt: Date.now(),
lastUsed: null,
});
}
});
storage.set(KEYS.accounts, accounts);
console.log(`✅ Imported ${imported.length} accounts.`);
} catch (e) {
console.error('❌ Invalid JSON format:', e.message);
}
},
// Help
help: () => {
console.log('');
console.log('📋 MULTI-ACCOUNT COMMANDS:');
console.log('');
console.log(' XActions.Accounts.add("username") - Add account');
console.log(' XActions.Accounts.remove("user") - Remove account');
console.log(' XActions.Accounts.list() - List all accounts');
console.log(' XActions.Accounts.current() - Get current account');
console.log(' XActions.Accounts.next() - Get next in rotation');
console.log(' XActions.Accounts.switch() - Open account switcher');
console.log(' XActions.Accounts.stats() - Show all stats');
console.log(' XActions.Accounts.setStatus("u","s") - Set status');
console.log(' XActions.Accounts.export() - Export account list');
console.log(' XActions.Accounts.import(json) - Import accounts');
console.log(' XActions.Accounts.clear() - Clear all data');
console.log('');
console.log('📊 STATUS VALUES: active, paused, limited, suspended');
console.log('');
}
};
// Auto-detect current account
if (CONFIG.autoDetect) {
const current = getCurrentUsername();
if (current) {
const accounts = window.XActions.Accounts.getAll();
if (!accounts.find(a => a.username === current)) {
console.log(`🔍 Detected current account: @${current}`);
console.log(' Run XActions.Accounts.add("' + current + '") to track it.');
} else {
window.XActions.Accounts.markUsed(current);
console.log(`👤 Current account: @${current}`);
}
}
}
console.log('✅ Multi-Account Manager loaded!');
console.log(' Run XActions.Accounts.help() for commands.');
console.log('');
})();
});
register("mute-by-keywords", function(){
var CONFIG = {
// Keywords to look for in bio (case-insensitive)
muteKeywords: [
'crypto',
'nft',
'giveaway',
'trading signals',
'dm for promo'
],
// Scroll settings
scrollDelay: 1500,
maxScrolls: 30,
maxRetries: 3,
// Delay between mutes
muteDelay: 2000,
// Dry run mode
dryRun: true,
// Max accounts to mute per run
maxMutes: 50
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function muteByKeywords() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Stop switch: run window.stopMuteByKeywords() from the console to abort
// after the account currently being processed.
let stopped = false;
window.stopMuteByKeywords = () => {
stopped = true;
console.log('🛑 Stop requested. Finishing the current account, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🔇 XActions — Mute By Keywords ║
║ Mute users with specific bio keywords ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - No accounts will be muted ║' : '║ 🔴 LIVE MODE - Accounts WILL be muted ║'}
╚══════════════════════════════════════════════════════════════╝
`);
console.log('💡 To stop early: window.stopMuteByKeywords()\n');
if (!window.location.pathname.includes('/followers') && !window.location.pathname.includes('/following')) {
console.error('❌ Please navigate to a followers or following page first!');
return;
}
console.log('🔍 Looking for users with these keywords:');
CONFIG.muteKeywords.forEach(kw => console.log(` • ${kw}`));
console.log('');
const $userCell = '[data-testid="UserCell"]';
const scanned = new Set();
const matches = [];
let retries = 0;
let scrollCount = 0;
while (!stopped && scrollCount < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const prevSize = scanned.size;
document.querySelectorAll($userCell).forEach(cell => {
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!username || scanned.has(username)) return;
scanned.add(username);
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const bio = (bioEl?.textContent || '').toLowerCase();
const matchedKeywords = CONFIG.muteKeywords.filter(kw =>
bio.includes(kw.toLowerCase())
);
if (matchedKeywords.length > 0) {
matches.push({
username,
bio: bioEl?.textContent || '',
keywords: matchedKeywords,
element: cell
});
}
});
if (scanned.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scanned.size} | Matches: ${matches.length}`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Scan complete!`);
console.log(` Total scanned: ${scanned.size}`);
console.log(` Matching users: ${matches.length}\n`);
if (matches.length === 0) {
console.log('🎉 No users found with those keywords!');
return;
}
console.log('═'.repeat(60));
console.log('🎯 USERS WITH MATCHING KEYWORDS');
console.log('═'.repeat(60));
matches.forEach((m, i) => {
console.log(`\n${i + 1}. @${m.username}`);
console.log(` Keywords: ${m.keywords.join(', ')}`);
console.log(` Bio: "${m.bio.slice(0, 100)}${m.bio.length > 100 ? '...' : ''}"`);
});
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE - No mutes performed');
console.log('Set CONFIG.dryRun = false to actually mute');
console.log('═'.repeat(60));
} else {
console.log('\n' + '═'.repeat(60));
console.log('🔇 MUTING MATCHING USERS');
console.log('═'.repeat(60));
let muted = 0;
const toMute = matches.slice(0, CONFIG.maxMutes);
for (const user of toMute) {
if (stopped) {
console.log('🛑 Stopped by user.');
break;
}
try {
user.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
const moreButton = user.element.querySelector('[data-testid="userActions"]');
if (moreButton) {
moreButton.click();
await sleep(500);
// Look for mute option
const muteOption = document.querySelector('[data-testid="mute"]');
if (muteOption) {
muteOption.click();
muted++;
console.log(`✅ Muted @${user.username}`);
} else {
console.log(`⚠️ Mute option not found for @${user.username}`);
}
// Close menu
document.body.click();
}
await sleep(CONFIG.muteDelay);
} catch (e) {
console.log(`❌ Failed to mute @${user.username}`);
}
}
console.log(`\n✅ Muted ${muted}/${toMute.length} accounts`);
}
// Save log
const storageKey = 'xactions_keyword_mutes';
const log = matches.map(m => ({
username: m.username,
keywords: m.keywords,
timestamp: new Date().toISOString()
}));
localStorage.setItem(storageKey, JSON.stringify(log));
console.log('\n💾 Results saved to localStorage');
})();
});
register("new-followers-alert", function(){
var CONFIG = {
// Scroll settings
scrollDelay: 2000,
maxScrolls: 100,
maxRetries: 5,
// Welcome message templates (customize these!)
welcomeMessages: [
"Hey {name}! Thanks for the follow! 🙏 Glad to connect!",
"Welcome {name}! 👋 Thanks for following! What brings you here?",
"Hey {name}! Appreciate the follow! Looking forward to connecting! 🚀",
"Thanks for following {name}! Always great to meet new people! ✨"
],
// Auto-download new followers list
autoDownload: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function newFollowersAlert() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $userCell = '[data-testid="UserCell"]';
const STORAGE_KEY = 'xactions_followers_snapshot';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🆕 NEW FOLLOWERS ALERT ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify page
if (!window.location.href.includes('/followers')) {
console.error('❌ ERROR: Must be on your Followers page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME/followers');
return;
}
console.log('🚀 Scanning followers...');
console.log('');
/**
* Get user info from cell
*/
function getUserInfo(cell) {
const link = cell.querySelector('a[href^="/"]');
const username = link ? link.getAttribute('href')?.replace('/', '').split('/')[0] : null;
const nameSpan = cell.querySelector('[dir="ltr"] span');
const displayName = nameSpan ? nameSpan.textContent : username;
return { username, displayName };
}
// Scrape followers
const followers = new Map(); // username -> displayName
let lastCount = 0;
let retries = 0;
let scrolls = 0;
while (scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
document.querySelectorAll($userCell).forEach(cell => {
const { username, displayName } = getUserInfo(cell);
if (username && !followers.has(username)) {
followers.set(username, displayName);
}
});
if (followers.size === lastCount) {
retries++;
} else {
retries = 0;
lastCount = followers.size;
}
console.log(`📊 Found ${followers.size} followers...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log(`✅ Total followers: ${followers.size}`);
console.log('');
// Load previous snapshot
let previous = null;
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) previous = JSON.parse(saved);
} catch (e) {}
const timestamp = new Date().toISOString();
// Save current
const snapshot = {
savedAt: timestamp,
count: followers.size,
followers: Object.fromEntries(followers)
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
if (!previous) {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📸 FIRST SNAPSHOT SAVED! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 ${followers.size} followers saved`);
console.log('');
console.log('💡 Run again later to see new followers!');
return;
}
// Find new followers
const prevSet = new Set(Object.keys(previous.followers));
const newFollowers = [];
const unfollowers = [];
followers.forEach((displayName, username) => {
if (!prevSet.has(username)) {
newFollowers.push({ username, displayName });
}
});
prevSet.forEach(username => {
if (!followers.has(username)) {
unfollowers.push({ username, displayName: previous.followers[username] });
}
});
// Display results
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📊 FOLLOWER CHANGES ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📅 Since: ${previous.savedAt}`);
console.log(`📈 Previous: ${previous.count} → Current: ${followers.size}`);
console.log('');
if (newFollowers.length > 0) {
console.log(`🆕 NEW FOLLOWERS (${newFollowers.length}):`);
console.log('');
newFollowers.forEach((f, i) => {
console.log(` ${i + 1}. ${f.displayName} (@${f.username})`);
console.log(` 🔗 https://x.com/${f.username}`);
// Generate welcome message
const template = CONFIG.welcomeMessages[i % CONFIG.welcomeMessages.length];
const message = template.replace('{name}', f.displayName.split(' ')[0]);
console.log(` 💬 "${message}"`);
console.log('');
});
} else {
console.log('📭 No new followers since last check.');
console.log('');
}
if (unfollowers.length > 0) {
console.log(`🚫 UNFOLLOWED (${unfollowers.length}):`);
unfollowers.forEach((f, i) => {
console.log(` ${i + 1}. ${f.displayName} (@${f.username})`);
});
console.log('');
}
// Download report
if (CONFIG.autoDownload && newFollowers.length > 0) {
let report = `NEW FOLLOWERS REPORT\n`;
report += `${'='.repeat(50)}\n`;
report += `Generated: ${timestamp}\n`;
report += `Since: ${previous.savedAt}\n`;
report += `New followers: ${newFollowers.length}\n`;
report += `${'='.repeat(50)}\n\n`;
report += `NEW FOLLOWERS:\n`;
report += `${'-'.repeat(30)}\n\n`;
newFollowers.forEach((f, i) => {
report += `${i + 1}. ${f.displayName} (@${f.username})\n`;
report += ` Profile: https://x.com/${f.username}\n`;
const template = CONFIG.welcomeMessages[i % CONFIG.welcomeMessages.length];
const message = template.replace('{name}', f.displayName.split(' ')[0]);
report += ` Welcome Message: ${message}\n`;
report += '\n';
});
if (unfollowers.length > 0) {
report += `\nUNFOLLOWERS (${unfollowers.length}):\n`;
report += `${'-'.repeat(30)}\n`;
unfollowers.forEach((f, i) => {
report += `${i + 1}. ${f.displayName} (@${f.username})\n`;
});
}
const blob = new Blob([report], { type: 'text/plain' });
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = `new_followers_${timestamp.split('T')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
console.log('💾 Report downloaded!');
}
// Copy welcome messages for easy pasting
if (newFollowers.length > 0) {
const messages = newFollowers.map((f, i) => {
const template = CONFIG.welcomeMessages[i % CONFIG.welcomeMessages.length];
return `@${f.username}: ${template.replace('{name}', f.displayName.split(' ')[0])}`;
}).join('\n\n');
try {
await navigator.clipboard.writeText(messages);
console.log('📋 Welcome messages copied to clipboard!');
} catch (e) {}
}
const result = { timestamp, newFollowers, unfollowers };
window.newFollowersResult = result;
console.log('');
console.log('💡 Access via: window.newFollowersResult');
return result;
})();
});
register("notification-manager", function(){
(async function notificationManager() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// 'markRead' -> clear the unread state. 'summary' -> scrape + export.
action: 'summary',
// ---- summary options ----
maxNotifications: 100, // cap for the summary scrape
exportResults: true, // download JSON + CSV when action is 'summary'
// ---- shared scrolling ----
maxScrollRounds: 30,
scrollDelay: 1400
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
cell: '[data-testid="cellInnerDiv"]',
notification: '[data-testid="notification"]',
tweet: 'article[data-testid="tweet"]',
verified: '[data-testid="icon-verified"]',
caret: '[data-testid="caret"]',
menuItem: '[role="menuitem"]',
confirm: '[data-testid="confirmationSheetConfirm"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const download = (blob, filename) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
console.log(`📥 Downloaded: ${filename}`);
};
const downloadJSON = (data, filename) =>
download(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), filename);
const downloadCSV = (rows, filename) => {
const esc = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
const headers = ['type', 'time', 'verified', 'text'];
const lines = [headers.join(',')];
for (const r of rows) {
lines.push([r.type, r.time, r.isVerified, r.text].map(esc).join(','));
}
download(new Blob([lines.join('\n')], { type: 'text/csv' }), filename);
};
const classify = (text) => {
const lc = text.toLowerCase();
if (lc.includes('liked your') || lc.includes(' liked ')) return 'like';
if (lc.includes('reposted') || lc.includes('retweeted')) return 'repost';
if (lc.includes('followed you')) return 'follow';
if (lc.includes('quoted')) return 'quote';
if (lc.includes('replied') || lc.includes('mentioned') || lc.includes(' tagged ')) return 'mention';
return 'other';
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔔 NOTIFICATION MANAGER - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard
if (!/\/notifications/.test(window.location.pathname)) {
log.error('Not on the Notifications page. Go to https://x.com/notifications and re-run.');
return;
}
// ============================================
// ✅ ACTION: MARK ALL AS READ
// ============================================
const markRead = async () => {
log.info('Attempting to mark all notifications as read...');
// Try the timeline overflow (...) menu first. X labels it via the caret testid.
let clickedMenu = false;
const carets = document.querySelectorAll(SELECTORS.caret);
// The header caret sits high on the page; take the top-most one.
const headerCaret = Array.from(carets)
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0];
if (headerCaret) {
try {
headerCaret.click();
await sleep(700);
const items = document.querySelectorAll(SELECTORS.menuItem);
for (const item of items) {
if (/mark all as read/i.test(item.textContent)) {
item.click();
clickedMenu = true;
await sleep(700);
const confirm = document.querySelector(SELECTORS.confirm);
if (confirm) { confirm.click(); await sleep(700); }
log.success('Clicked "Mark all as read".');
break;
}
}
if (!clickedMenu) {
// Close the menu we opened.
document.body.click();
await sleep(300);
}
} catch (e) {
log.warning(`Overflow menu path failed: ${e.message}`);
}
}
if (!clickedMenu) {
log.info('No "Mark all as read" menu item found. Falling back to scroll-to-view.');
}
// Scrolling the feed makes X mark notifications read server-side on view.
let seen = 0;
let rounds = 0;
let lastHeight = -1;
while (rounds < CONFIG.maxScrollRounds) {
seen = document.querySelectorAll(`${SELECTORS.cell}, ${SELECTORS.notification}, ${SELECTORS.tweet}`).length;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
rounds++;
if (document.body.scrollHeight === lastHeight) break;
lastHeight = document.body.scrollHeight;
}
window.scrollTo(0, 0);
log.success(`Done. Menu action: ${clickedMenu ? 'yes' : 'no'}. Notifications viewed: ${seen}.`);
log.info('X marks notifications read as they scroll into view; the blue badge should clear shortly.');
};
// ============================================
// 📊 ACTION: SUMMARY (SCRAPE + EXPORT)
// ============================================
const summary = async () => {
log.info(`Scraping up to ${CONFIG.maxNotifications} notifications...`);
const seen = new Map();
let rounds = 0;
let empty = 0;
while (seen.size < CONFIG.maxNotifications && rounds < CONFIG.maxScrollRounds && empty < 5) {
const before = seen.size;
const cells = document.querySelectorAll(`${SELECTORS.cell}, ${SELECTORS.notification}, ${SELECTORS.tweet}`);
for (const cell of cells) {
const text = (cell.textContent || '').trim().slice(0, 280);
if (!text || text.length < 5) continue;
const time = cell.querySelector('time')?.getAttribute('datetime') || '';
const key = text.slice(0, 90) + time;
if (seen.has(key)) continue;
seen.set(key, {
type: classify(text),
text,
time,
isVerified: !!cell.querySelector(SELECTORS.verified)
});
}
if (seen.size === before) empty++;
else empty = 0;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
rounds++;
}
window.scrollTo(0, 0);
const all = [...seen.values()].slice(0, CONFIG.maxNotifications);
if (all.length === 0) {
log.warning('No notifications found to summarize.');
return;
}
const byType = {};
for (const n of all) byType[n.type] = (byType[n.type] || 0) + 1;
console.log(`\n🔔 Total notifications: ${all.length}\n📊 BY TYPE:`);
const emoji = { like: '❤️', repost: '🔁', follow: '👤', mention: '💬', quote: '💭', other: '📌' };
Object.entries(byType).sort((a, b) => b[1] - a[1]).forEach(([t, c]) => {
console.log(` ${emoji[t] || '📌'} ${t}: ${c}`);
});
if (CONFIG.exportResults) {
const date = new Date().toISOString().slice(0, 10);
downloadJSON(
{ exportedAt: new Date().toISOString(), total: all.length, byType, notifications: all },
`xactions-notifications-${date}.json`
);
downloadCSV(all, `xactions-notifications-${date}.csv`);
}
};
// ============================================
// 🎯 DISPATCH
// ============================================
try {
if (CONFIG.action === 'markRead') await markRead();
else if (CONFIG.action === 'summary') await summary();
else log.error(`Unknown action "${CONFIG.action}". Use 'markRead' or 'summary'.`);
} catch (error) {
log.error(`Fatal: ${error.message}`);
}
log.success('Script completed! by nichxbt');
})();
});
register("pin-tweet", function(){
(async function pinTweet() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// 'pin' or 'unpin'.
action: 'pin',
// Optional: the URL (or status id) of the tweet to act on. Must be your own
// tweet. Leave '' to use the first tweet shown on the current page.
// Example: 'https://x.com/nichxbt/status/1234567890123456789'
tweetUrl: '',
// Preview only: log what would happen without opening the menu.
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
article: 'article[data-testid="tweet"]',
caret: '[data-testid="caret"]',
tweetText: '[data-testid="tweetText"]',
socialContext: '[data-testid="socialContext"]',
confirm: '[data-testid="confirmationSheetConfirm"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const waitForSelector = async (selector, timeoutMs = 4000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(200);
}
return null;
};
const extractStatusId = (urlOrId) => {
if (!urlOrId) return '';
const m = String(urlOrId).match(/status\/(\d+)/);
if (m) return m[1];
const digits = String(urlOrId).match(/^\d{5,}$/);
return digits ? digits[0] : '';
};
// Locate the article for a given status id, scrolling to find it if needed.
const findArticleById = async (statusId) => {
for (let i = 0; i < 12; i++) {
for (const a of document.querySelectorAll(SELECTORS.article)) {
const link = a.querySelector('a[href*="/status/"]');
const href = link ? link.getAttribute('href') : '';
if (href && href.includes(`/status/${statusId}`)) return a;
}
window.scrollBy(0, window.innerHeight * 0.8);
await sleep(700);
}
return null;
};
// Pick the Pin or Unpin item from the open tweet menu.
const findMenuItem = (wantPin) => {
const items = Array.from(document.querySelectorAll('[role="menuitem"]'));
if (wantPin) {
return items.find((el) => /(^|\s)pin/i.test(el.textContent || '') && !/unpin/i.test(el.textContent || '')) || null;
}
return items.find((el) => /unpin/i.test(el.textContent || '')) || null;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const action = (CONFIG.action || '').toLowerCase();
const stats = { action, targetFound: false, menuOpened: false, done: false, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📌 PIN TWEET - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
try {
const host = window.location.hostname;
if (!/(^|\.)x\.com$/.test(host) && !/(^|\.)twitter\.com$/.test(host)) {
log.warning('Not on x.com. Open your profile page, then re-run.');
return stats;
}
if (action !== 'pin' && action !== 'unpin') {
log.error(`CONFIG.action must be "pin" or "unpin". Got "${CONFIG.action}".`);
return stats;
}
// Resolve the target article.
let article = null;
const statusId = extractStatusId(CONFIG.tweetUrl);
if (statusId) {
log.info(`Target status id: ${statusId}`);
article = await findArticleById(statusId);
if (!article) {
log.error(`Tweet ${statusId} not found on this page. Open its status page or your profile and re-run.`);
stats.errors++;
return stats;
}
} else {
article = document.querySelector(SELECTORS.article);
if (!article) {
log.error('No tweet found on this page. Open your profile (x.com/YOUR_USERNAME) and re-run.');
stats.errors++;
return stats;
}
log.info('No tweetUrl set. Using the first tweet on the page.');
}
stats.targetFound = true;
const preview = (article.querySelector(SELECTORS.tweetText)?.textContent || '').slice(0, 60);
const ctx = article.querySelector(SELECTORS.socialContext)?.textContent || '';
const isPinned = /pinned/i.test(ctx);
log.info(`Target: "${preview}${preview.length >= 60 ? '...' : ''}"`);
// Guard against no-op actions.
if (action === 'unpin' && !isPinned && !statusId) {
log.warning('The first tweet is not marked pinned. Nothing to unpin.');
return stats;
}
if (action === 'pin' && isPinned) {
log.success('This tweet is already pinned. Nothing to do.');
stats.done = true;
return stats;
}
if (CONFIG.dryRun) {
log.warning(`DRY RUN. Would ${action} this tweet. Set CONFIG.dryRun = false to do it.`);
return stats;
}
const caret = article.querySelector(SELECTORS.caret);
if (!caret) {
log.error('Tweet menu (caret) button not found on the target tweet.');
stats.errors++;
return stats;
}
caret.scrollIntoView({ block: 'center' });
await sleep(300);
caret.click();
await sleep(800);
stats.menuOpened = true;
const item = findMenuItem(action === 'pin');
if (!item) {
log.error(`"${action === 'pin' ? 'Pin' : 'Unpin'}" option not found in the menu. Is this your own tweet?`);
document.body.click();
stats.errors++;
return stats;
}
item.click();
await sleep(800);
// Pinning shows a confirmation sheet; unpin sometimes does too.
const confirm = await waitForSelector(SELECTORS.confirm, 2500);
if (confirm) {
confirm.click();
await sleep(800);
}
stats.done = true;
log.success(`Tweet ${action === 'pin' ? 'pinned' : 'unpinned'}.`);
} catch (error) {
log.error(`Error: ${error.message}`);
stats.errors++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 PIN TWEET - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🎯 Action: ${String(stats.action).padEnd(32)}║
║ 🔎 Target found: ${String(stats.targetFound).padEnd(32)}║
║ ✅ Done: ${String(stats.done).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("post-thread", function(){
(async function postThread() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Each string is one tweet in the thread, top to bottom.
tweets: [
'Thread part 1 🧵',
'Thread part 2: the details.',
'Thread part 3: wrap up. Follow for more.'
],
// Prepend "1/N" numbering to each tweet automatically.
autoNumber: false,
// Max characters per tweet before the script refuses to post.
maxChars: 280,
// Milliseconds to wait after adding each new tweet box.
delayBetween: 2000,
// Preview only: validate and log without clicking Post.
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
composer: '[data-testid="tweetTextarea_0"]',
anyComposer: '[data-testid^="tweetTextarea_"]',
newTweetButton: 'a[data-testid="SideNav_NewTweet_Button"]',
addButton: '[data-testid="addButton"]',
postButton: '[data-testid="tweetButton"]',
postButtonInline: '[data-testid="tweetButtonInline"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
// Real input dispatch into X's contenteditable composer.
const typeIntoComposer = async (el, text) => {
el.focus();
await sleep(150);
const inserted = document.execCommand('insertText', false, text);
if (!inserted) {
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));
el.textContent = text;
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: text, bubbles: true }));
}
await sleep(300);
};
const waitForSelector = async (selector, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(200);
}
return null;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { total: 0, typed: 0, posted: false, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🧵 POST THREAD - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
try {
const host = window.location.hostname;
if (!/(^|\.)x\.com$/.test(host) && !/(^|\.)twitter\.com$/.test(host)) {
log.warning('Not on x.com. Open x.com/compose/post, then re-run.');
return stats;
}
const raw = (CONFIG.tweets || []).map((t) => (t || '').trim()).filter(Boolean);
if (raw.length === 0) {
log.error('CONFIG.tweets is empty. Add at least one tweet string.');
return stats;
}
// Apply numbering, then validate lengths.
const tweets = raw.map((t, i) => (CONFIG.autoNumber ? `${i + 1}/${raw.length} ${t}` : t));
stats.total = tweets.length;
let overLimit = false;
tweets.forEach((t, i) => {
const status = t.length > CONFIG.maxChars ? '🔴 OVER' : '🟢 OK';
log.info(`Tweet ${i + 1}/${tweets.length} [${t.length}/${CONFIG.maxChars}] ${status}`);
if (t.length > CONFIG.maxChars) overLimit = true;
});
if (overLimit) {
log.error(`One or more tweets exceed ${CONFIG.maxChars} characters. Fix them and re-run.`);
return stats;
}
if (CONFIG.dryRun) {
log.warning('DRY RUN. Nothing posted. Set CONFIG.dryRun = false to post for real.');
return stats;
}
// Open the composer if needed.
let firstBox = document.querySelector(SELECTORS.composer);
if (!firstBox) {
const newBtn = document.querySelector(SELECTORS.newTweetButton);
if (newBtn) {
newBtn.click();
firstBox = await waitForSelector(SELECTORS.composer, 6000);
}
}
if (!firstBox) {
log.error('Composer not found. Open x.com/compose/post and re-run.');
stats.errors++;
return stats;
}
await typeIntoComposer(firstBox, tweets[0]);
stats.typed = 1;
log.success(`Tweet 1/${tweets.length} typed.`);
// Add each subsequent tweet via the "+" add button, then type into the newest box.
for (let i = 1; i < tweets.length; i++) {
const addBtn = document.querySelector(SELECTORS.addButton);
if (!addBtn) {
log.error(`Add-tweet button not found before tweet ${i + 1}. Stopping.`);
stats.errors++;
break;
}
addBtn.click();
await sleep(CONFIG.delayBetween);
const boxes = document.querySelectorAll(SELECTORS.anyComposer);
const newBox = boxes[boxes.length - 1];
if (!newBox) {
log.error(`New composer box not found for tweet ${i + 1}. Stopping.`);
stats.errors++;
break;
}
await typeIntoComposer(newBox, tweets[i]);
stats.typed++;
log.success(`Tweet ${i + 1}/${tweets.length} typed.`);
}
await sleep(800);
const postBtn =
document.querySelector(SELECTORS.postButton) ||
document.querySelector(SELECTORS.postButtonInline);
if (!postBtn) {
log.error('Post button not found. Thread is typed. Click "Post all" manually.');
stats.errors++;
return stats;
}
if (postBtn.getAttribute('aria-disabled') === 'true' || postBtn.disabled) {
log.error('Post button is disabled. Check the composer for an empty or over-limit tweet.');
stats.errors++;
return stats;
}
postBtn.click();
await sleep(3000);
const stillOpen = document.querySelector(SELECTORS.composer);
const openText = stillOpen ? (stillOpen.textContent || '').trim() : '';
if (!stillOpen || openText.length === 0) {
stats.posted = true;
log.success(`Thread posted: ${stats.typed} tweet(s).`);
} else {
log.warning('Composer still open. The thread may not have posted. Check the page.');
stats.errors++;
}
} catch (error) {
log.error(`Error: ${error.message}`);
stats.errors++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 POST THREAD - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🧵 Tweets in thread: ${String(stats.total).padEnd(32)}║
║ ⌨️ Typed: ${String(stats.typed).padEnd(32)}║
║ ✅ Posted: ${String(stats.posted).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("post-tweet", function(){
(async function postTweet() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// The text to post (max 280 for non-Premium, longer allowed on Premium).
text: 'gm from XActions 🚀',
// Optional: URL of a tweet to reply to. Leave '' to post a standalone tweet.
// Example: 'https://x.com/nichxbt/status/1234567890123456789'
replyToUrl: '',
// Optional note about media. This script types text only; attach any image
// or video by hand in the composer before it clicks Post. Set a value here
// (e.g. '1 image') and the script will PAUSE for you to attach it.
mediaNote: '',
// Seconds to wait for you to attach media when mediaNote is set.
mediaWaitSeconds: 15,
// Preview only: log what would happen without clicking Post.
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
composer: '[data-testid="tweetTextarea_0"]',
newTweetButton: 'a[data-testid="SideNav_NewTweet_Button"]',
inlineReply: '[data-testid="tweetTextarea_0"]',
postButtonInline: '[data-testid="tweetButtonInline"]',
postButton: '[data-testid="tweetButton"]',
replyButton: '[data-testid="reply"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
// Type into X's contenteditable composer with a real input dispatch so React
// registers the value. focus + execCommand('insertText') is what X's own
// editor listens for; a plain .textContent assignment does not update state.
const typeIntoComposer = async (el, text) => {
el.focus();
await sleep(150);
const inserted = document.execCommand('insertText', false, text);
if (!inserted) {
// Fallback for environments where execCommand is a no-op: dispatch a
// beforeinput/input pair after setting text on the editable node.
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));
el.textContent = text;
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: text, bubbles: true }));
}
await sleep(300);
};
const waitForSelector = async (selector, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(200);
}
return null;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { posted: 0, mode: CONFIG.replyToUrl ? 'reply' : 'tweet', errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📝 POST TWEET - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
try {
if (!CONFIG.text || !CONFIG.text.trim()) {
log.error('CONFIG.text is empty. Set the text you want to post.');
return stats;
}
const host = window.location.hostname;
if (!/(^|\.)x\.com$/.test(host) && !/(^|\.)twitter\.com$/.test(host)) {
log.warning('Not on x.com. Open x.com/home or x.com/compose/post, then re-run.');
return stats;
}
log.info(`Mode: ${stats.mode}`);
log.info(`Length: ${CONFIG.text.length} characters`);
// If replying, navigate to the target tweet and open its reply composer.
if (CONFIG.replyToUrl) {
const onTarget = window.location.href.split('?')[0] === CONFIG.replyToUrl.split('?')[0];
if (!onTarget) {
log.warning(`Open the tweet you want to reply to first: ${CONFIG.replyToUrl}`);
log.warning('Then re-run this script on that page.');
return stats;
}
const replyBtn = document.querySelector(SELECTORS.replyButton);
if (replyBtn) {
replyBtn.click();
await sleep(1200);
}
}
// Open the composer if it is not already present.
let composer = document.querySelector(SELECTORS.composer);
if (!composer) {
const newBtn = document.querySelector(SELECTORS.newTweetButton);
if (newBtn) {
newBtn.click();
composer = await waitForSelector(SELECTORS.composer, 6000);
}
}
if (!composer) {
log.error('Composer not found. Open x.com/compose/post and re-run.');
stats.errors++;
return stats;
}
await typeIntoComposer(composer, CONFIG.text);
log.success('Text typed into composer.');
if (CONFIG.mediaNote) {
log.warning(`Media note set ("${CONFIG.mediaNote}"). Attach it now.`);
log.info(`Waiting ${CONFIG.mediaWaitSeconds}s for you to attach media...`);
await sleep(CONFIG.mediaWaitSeconds * 1000);
}
if (CONFIG.dryRun) {
log.warning('DRY RUN. Nothing posted. Set CONFIG.dryRun = false to post for real.');
log.info(`Would post: "${CONFIG.text.slice(0, 80)}${CONFIG.text.length > 80 ? '...' : ''}"`);
return stats;
}
await sleep(400);
const postBtn =
document.querySelector(SELECTORS.postButtonInline) ||
document.querySelector(SELECTORS.postButton);
if (!postBtn) {
log.error('Post button not found. Text is typed. Click Post manually.');
stats.errors++;
return stats;
}
if (postBtn.getAttribute('aria-disabled') === 'true' || postBtn.disabled) {
log.error('Post button is disabled (empty or over the character limit). Check your text.');
stats.errors++;
return stats;
}
postBtn.click();
await sleep(2500);
// Verify: a successful post clears the composer.
const stillThere = document.querySelector(SELECTORS.composer);
const composerText = stillThere ? (stillThere.textContent || '').trim() : '';
if (!stillThere || composerText.length === 0) {
stats.posted = 1;
log.success(`Posted ${stats.mode}: "${CONFIG.text.slice(0, 60)}${CONFIG.text.length > 60 ? '...' : ''}"`);
} else {
log.warning('Composer still has text. The post may not have gone through. Check the page.');
stats.errors++;
}
} catch (error) {
log.error(`Error: ${error.message}`);
stats.errors++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 POST TWEET - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 📝 Mode: ${String(stats.mode).padEnd(32)}║
║ ✅ Posted: ${String(stats.posted).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("profile-stats", function(){
(async function profileStats() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 📊 XActions — Profile Stats ║
║ Get comprehensive profile statistics ║
╚══════════════════════════════════════════════════════════════╝
`);
const parseCount = (str) => {
if (!str) return 0;
str = str.replace(/,/g, '').trim();
const match = str.match(/([\d.]+)([KMB])?/i);
if (match) {
let num = parseFloat(match[1]);
const multipliers = { 'K': 1000, 'M': 1000000, 'B': 1000000000 };
if (match[2]) num *= multipliers[match[2].toUpperCase()];
return Math.round(num);
}
return 0;
};
const formatNum = (n) => {
if (n >= 1000000) return (n / 1000000).toFixed(2) + 'M';
if (n >= 1000) return (n / 1000).toFixed(2) + 'K';
return n.toString();
};
const username = window.location.pathname.match(/^\/([^\/]+)/)?.[1];
if (!username || ['home', 'explore', 'notifications', 'messages', 'i'].includes(username)) {
console.error('❌ Please navigate to a profile page first!');
return;
}
console.log(`📊 Gathering stats for @${username}...\n`);
// Gather profile information
const stats = {
username,
timestamp: new Date().toISOString(),
displayName: '',
bio: '',
location: '',
website: '',
joinDate: '',
followers: 0,
following: 0,
verified: false,
profileImage: '',
bannerImage: ''
};
// Display name
const nameEl = document.querySelector('[data-testid="UserName"]');
if (nameEl) {
const nameParts = nameEl.textContent.split('@');
stats.displayName = nameParts[0].trim();
}
// Bio
const bioEl = document.querySelector('[data-testid="UserDescription"]');
stats.bio = bioEl?.textContent || '';
// Location
const locationEl = document.querySelector('[data-testid="UserLocation"]');
stats.location = locationEl?.textContent || '';
// Website
const urlEl = document.querySelector('[data-testid="UserUrl"]');
stats.website = urlEl?.getAttribute('href') || urlEl?.querySelector('a')?.getAttribute('href') || urlEl?.textContent || '';
// Join date
const joinEl = document.querySelector('[data-testid="UserJoinDate"]');
stats.joinDate = joinEl?.textContent || '';
// Followers/Following
document.querySelectorAll('a').forEach(link => {
const href = link.getAttribute('href') || '';
const text = link.textContent || '';
if (href.includes('/followers') || href.includes('/verified_followers')) {
const match = text.match(/([\d,.]+[KMB]?)/);
if (match) stats.followers = parseCount(match[1]);
}
if (href.endsWith('/following')) {
const match = text.match(/([\d,.]+[KMB]?)/);
if (match) stats.following = parseCount(match[1]);
}
});
// Verified status
const verifiedBadge = document.querySelector('[data-testid="UserName"] svg[aria-label*="Verified"], [data-testid="UserName"] svg[aria-label*="verified"]');
stats.verified = verifiedBadge !== null;
// Profile image
const profileImg = document.querySelector('a[href$="/photo"] img');
stats.profileImage = profileImg?.src || '';
stats.hasDefaultAvatar = stats.profileImage.includes('default_profile');
// Banner image
const bannerImg = document.querySelector('a[href$="/header_photo"] img');
stats.bannerImage = bannerImg?.src || '';
stats.hasBanner = !!stats.bannerImage;
// Calculate derived stats
stats.ratio = stats.following > 0 ? (stats.followers / stats.following).toFixed(2) : 'N/A';
// Account age (approximate)
if (stats.joinDate) {
const joinMatch = stats.joinDate.match(/Joined\s+(\w+)\s+(\d{4})/);
if (joinMatch) {
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const monthIndex = monthNames.findIndex(m => m.startsWith(joinMatch[1]));
if (monthIndex >= 0) {
const joinDate = new Date(parseInt(joinMatch[2]), monthIndex, 1);
const now = new Date();
const years = (now - joinDate) / (1000 * 60 * 60 * 24 * 365);
stats.accountAgeYears = years.toFixed(1);
}
}
}
// Output
console.log('═'.repeat(60));
console.log(`📊 PROFILE STATISTICS: @${username}`);
console.log('═'.repeat(60));
console.log('\n👤 BASIC INFO:');
console.log('─'.repeat(50));
console.log(` Display Name: ${stats.displayName}`);
console.log(` Username: @${stats.username}`);
console.log(` Verified: ${stats.verified ? '✅ Yes' : '❌ No'}`);
if (stats.bio) {
console.log(`\n Bio:`);
stats.bio.split('\n').forEach(line => {
console.log(` "${line}"`);
});
}
if (stats.location) console.log(`\n 📍 Location: ${stats.location}`);
if (stats.website) console.log(` 🔗 Website: ${stats.website}`);
if (stats.joinDate) console.log(` 📅 ${stats.joinDate}`);
if (stats.accountAgeYears) console.log(` ⏰ Account Age: ${stats.accountAgeYears} years`);
console.log('\n📊 METRICS:');
console.log('─'.repeat(50));
console.log(` Followers: ${formatNum(stats.followers)}`);
console.log(` Following: ${formatNum(stats.following)}`);
console.log(` Ratio: ${stats.ratio} (followers/following)`);
// Quality indicators
console.log('\n🔍 PROFILE QUALITY INDICATORS:');
console.log('─'.repeat(50));
const indicators = [];
if (stats.verified) indicators.push('✅ Verified account');
else indicators.push('⬜ Not verified');
if (!stats.hasDefaultAvatar) indicators.push('✅ Custom profile picture');
else indicators.push('⚠️ Default profile picture');
if (stats.hasBanner) indicators.push('✅ Has banner image');
else indicators.push('⬜ No banner image');
if (stats.bio && stats.bio.length > 50) indicators.push('✅ Detailed bio');
else if (stats.bio) indicators.push('⚠️ Short bio');
else indicators.push('❌ No bio');
if (stats.website) indicators.push('✅ Has website link');
else indicators.push('⬜ No website');
if (stats.location) indicators.push('✅ Location set');
else indicators.push('⬜ No location');
const ratio = parseFloat(stats.ratio) || 0;
if (ratio > 1) indicators.push(`✅ Good follower ratio (${ratio})`);
else if (ratio > 0.5) indicators.push(`⚠️ Low follower ratio (${ratio})`);
else if (ratio > 0) indicators.push(`❌ Very low ratio (${ratio})`);
if (stats.accountAgeYears > 2) indicators.push(`✅ Established account (${stats.accountAgeYears} years)`);
else if (stats.accountAgeYears > 0.5) indicators.push(`⬜ Newer account (${stats.accountAgeYears} years)`);
else if (stats.accountAgeYears) indicators.push(`⚠️ Very new account (${stats.accountAgeYears} years)`);
indicators.forEach(ind => console.log(` ${ind}`));
// Score calculation
let score = 0;
if (stats.verified) score += 20;
if (!stats.hasDefaultAvatar) score += 15;
if (stats.hasBanner) score += 10;
if (stats.bio && stats.bio.length > 50) score += 15;
else if (stats.bio) score += 5;
if (stats.website) score += 10;
if (stats.location) score += 5;
if (ratio > 1) score += 15;
else if (ratio > 0.5) score += 5;
if (stats.accountAgeYears > 2) score += 10;
else if (stats.accountAgeYears > 0.5) score += 5;
console.log('\n📈 PROFILE COMPLETENESS SCORE:');
console.log('─'.repeat(50));
const scoreBar = '█'.repeat(Math.round(score / 5)) + '░'.repeat(20 - Math.round(score / 5));
console.log(` [${scoreBar}] ${score}/100`);
if (score >= 80) console.log(' 🏆 Excellent profile!');
else if (score >= 60) console.log(' 👍 Good profile');
else if (score >= 40) console.log(' ⚠️ Profile needs improvement');
else console.log(' ❌ Incomplete profile');
// Bio analysis
if (stats.bio) {
console.log('\n📝 BIO ANALYSIS:');
console.log('─'.repeat(50));
console.log(` Length: ${stats.bio.length} characters`);
// Check for keywords
const bioLower = stats.bio.toLowerCase();
const keywords = {
'crypto': ['crypto', 'bitcoin', 'btc', 'eth', 'nft', 'defi', 'web3', 'blockchain'],
'tech': ['developer', 'engineer', 'programmer', 'coder', 'tech', 'ai', 'software'],
'marketing': ['marketing', 'growth', 'seo', 'social media', 'brand'],
'creator': ['creator', 'content', 'youtuber', 'podcaster', 'writer'],
'business': ['founder', 'ceo', 'entrepreneur', 'startup', 'investor']
};
const foundCategories = [];
Object.entries(keywords).forEach(([category, words]) => {
if (words.some(w => bioLower.includes(w))) {
foundCategories.push(category);
}
});
if (foundCategories.length > 0) {
console.log(` Categories: ${foundCategories.join(', ')}`);
}
// Links in bio
const links = stats.bio.match(/https?:\/\/\S+/g) || [];
if (links.length > 0) {
console.log(` Links found: ${links.length}`);
}
// Hashtags in bio
const hashtags = stats.bio.match(/#\w+/g) || [];
if (hashtags.length > 0) {
console.log(` Hashtags: ${hashtags.join(' ')}`);
}
}
// Save to localStorage
const storageKey = `xactions_profile_${username}`;
const history = JSON.parse(localStorage.getItem(storageKey) || '[]');
history.push(stats);
// Keep last 30 snapshots
const trimmedHistory = history.slice(-30);
localStorage.setItem(storageKey, JSON.stringify(trimmedHistory));
// Show historical comparison if available
if (history.length > 1) {
const oldest = history[0];
const followerChange = stats.followers - oldest.followers;
const daysDiff = (new Date(stats.timestamp) - new Date(oldest.timestamp)) / (1000 * 60 * 60 * 24);
if (daysDiff > 0) {
console.log('\n📈 HISTORICAL COMPARISON:');
console.log('─'.repeat(50));
console.log(` Tracking since: ${new Date(oldest.timestamp).toLocaleDateString()}`);
console.log(` Follower change: ${followerChange >= 0 ? '+' : ''}${formatNum(followerChange)}`);
console.log(` Daily average: ${(followerChange / daysDiff).toFixed(1)} followers/day`);
}
}
console.log('\n' + '═'.repeat(60));
console.log(`💾 Snapshot saved (${trimmedHistory.length} total)`);
console.log(`📥 Export: copy(localStorage.getItem("${storageKey}"))`);
console.log('═'.repeat(60) + '\n');
// Return stats object for programmatic use
return stats;
})();
});
register("protect-active-users", function(){
var CONFIG = {
// Number of your recent posts to scan
postsToScan: 10,
// Which engagement types to track
engagementTypes: {
likers: true,
repliers: true,
retweeters: true,
quoters: false // Requires navigating to quote tweets
},
// Only protect users who engaged within this many days
lookbackDays: 30,
// Minimum engagements to be protected
minEngagements: 1,
// Scroll delay when loading lists
scrollDelay: 1500,
// Max scrolls per engagement list
maxScrollsPerList: 10
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function protectActiveUsers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Navigate within the SPA. Assigning window.location.href triggers a full
// page load, which destroys this console script before the first scan.
const spaNavigate = (url) => {
try {
const target = new URL(url, window.location.href);
if (target.origin === window.location.origin) {
window.history.pushState({}, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
return;
}
} catch (e) {}
window.location.href = url;
};
const $tweet = 'article[data-testid="tweet"]';
const $userCell = '[data-testid="UserCell"]';
const STORAGE_KEY = 'xactions_protected_users';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🛡️ PROTECT ACTIVE USERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Get username from URL
const pathMatch = window.location.pathname.match(/^\/([^\/]+)/);
const myUsername = pathMatch ? pathMatch[1] : null;
if (!myUsername || ['home', 'explore', 'search'].includes(myUsername)) {
console.error('❌ ERROR: Must be on YOUR profile page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME');
return;
}
console.log(`👤 Scanning posts from: @${myUsername}`);
console.log(`📊 Posts to scan: ${CONFIG.postsToScan}`);
console.log('');
// Engagement tracking
const engagementMap = new Map(); // username -> { count, types, tweets }
/**
* Get username from user cell
*/
function getUsername(cell) {
const link = cell.querySelector('a[href^="/"]');
return link ? link.getAttribute('href').replace('/', '').split('/')[0] : null;
}
/**
* Scrape users from current page (likes/retweets list)
*/
async function scrapeUsers(type) {
const users = new Set();
let scrolls = 0;
while (scrolls < CONFIG.maxScrollsPerList) {
document.querySelectorAll($userCell).forEach(cell => {
const username = getUsername(cell);
if (username && username !== myUsername) {
users.add(username);
}
});
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
return [...users];
}
/**
* Add engagement for users
*/
function addEngagement(users, type, tweetUrl) {
users.forEach(username => {
if (!engagementMap.has(username)) {
engagementMap.set(username, {
count: 0,
types: new Set(),
tweets: []
});
}
const data = engagementMap.get(username);
data.count++;
data.types.add(type);
if (!data.tweets.includes(tweetUrl)) {
data.tweets.push(tweetUrl);
}
});
}
// Get my recent tweets
console.log('🔍 Finding your recent posts...');
console.log('');
const myTweets = [];
const seenTweets = new Set();
let scrolls = 0;
while (myTweets.length < CONFIG.postsToScan && scrolls < 20) {
document.querySelectorAll($tweet).forEach(tweet => {
// Check if it's my tweet
const authorLink = tweet.querySelector('a[href^="/"][role="link"]');
const author = authorLink ? authorLink.getAttribute('href').replace('/', '').split('/')[0].toLowerCase() : null;
if (author !== myUsername.toLowerCase()) return;
// Get tweet ID. Prefer the permalink around the timestamp: the first
// /status/ link can belong to a quoted tweet.
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
const tweetLink = timeAnchor || tweet.querySelector('a[href*="/status/"]');
if (!tweetLink) return;
const match = tweetLink.href.match(/\/status\/(\d+)/);
if (!match || seenTweets.has(match[1])) return;
// Check if it's a retweet: reposts render socialContext inside an <a>;
// pinned posts don't. Structural check works on any UI language.
const socialContext = tweet.querySelector('[data-testid="socialContext"]');
const isRetweet = !!socialContext && socialContext.closest('a') !== null;
if (isRetweet) return;
seenTweets.add(match[1]);
myTweets.push({
id: match[1],
url: `https://x.com/${myUsername}/status/${match[1]}`
});
});
window.scrollTo(0, document.body.scrollHeight);
await sleep(1000);
scrolls++;
}
console.log(`📊 Found ${myTweets.length} of your posts`);
console.log('');
// Scan each tweet for engagers
for (let i = 0; i < Math.min(myTweets.length, CONFIG.postsToScan); i++) {
const tweet = myTweets[i];
console.log(`🔍 Scanning post ${i + 1}/${CONFIG.postsToScan}: ${tweet.url}`);
// Navigate to tweet (SPA navigation keeps this script alive)
spaNavigate(tweet.url);
await sleep(2000);
// Get likers
if (CONFIG.engagementTypes.likers) {
const likesLink = document.querySelector('a[href$="/likes"]');
if (likesLink) {
likesLink.click();
await sleep(1500);
const likers = await scrapeUsers('like');
addEngagement(likers, 'like', tweet.url);
console.log(` ❤️ Found ${likers.length} likers`);
window.history.back();
await sleep(1000);
}
}
// Get retweeters
if (CONFIG.engagementTypes.retweeters) {
const retweetsLink = document.querySelector('a[href$="/retweets"]');
if (retweetsLink) {
retweetsLink.click();
await sleep(1500);
const retweeters = await scrapeUsers('retweet');
addEngagement(retweeters, 'retweet', tweet.url);
console.log(` 🔄 Found ${retweeters.length} retweeters`);
window.history.back();
await sleep(1000);
}
}
// Get repliers (from the tweet page itself)
if (CONFIG.engagementTypes.repliers) {
const repliers = new Set();
document.querySelectorAll($tweet).forEach(replyTweet => {
const authorLink = replyTweet.querySelector('a[href^="/"][role="link"]');
const author = authorLink ? authorLink.getAttribute('href').replace('/', '').split('/')[0] : null;
if (author && author.toLowerCase() !== myUsername.toLowerCase()) {
repliers.add(author);
}
});
addEngagement([...repliers], 'reply', tweet.url);
console.log(` 💬 Found ${repliers.size} repliers`);
}
}
// Filter by minimum engagements
const protectedUsers = [];
engagementMap.forEach((data, username) => {
if (data.count >= CONFIG.minEngagements) {
protectedUsers.push({
username,
engagementCount: data.count,
engagementTypes: [...data.types],
tweets: data.tweets
});
}
});
// Sort by engagement count
protectedUsers.sort((a, b) => b.engagementCount - a.engagementCount);
// Save to localStorage
const result = {
savedAt: new Date().toISOString(),
myUsername,
postsScanned: Math.min(myTweets.length, CONFIG.postsToScan),
totalProtected: protectedUsers.length,
users: protectedUsers
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(result));
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ PROTECTION LIST SAVED! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`🛡️ Protected users: ${protectedUsers.length}`);
console.log('');
console.log('🏆 TOP ENGAGED USERS:');
protectedUsers.slice(0, 10).forEach((u, i) => {
console.log(` ${i + 1}. @${u.username} (${u.engagementCount} engagements: ${u.engagementTypes.join(', ')})`);
});
console.log('');
console.log('💡 smart-unfollow.js will respect this list!');
console.log('💡 Access via: window.protectedUsers');
window.protectedUsers = result;
return result;
})();
});
register("quote-tweet", function(){
(async function quoteTweet() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// The text of your quote tweet.
text: 'This is the take of the day 💯',
// Preview only: log what would happen without posting.
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
article: 'article[data-testid="tweet"]',
retweet: '[data-testid="retweet"]',
unretweet: '[data-testid="unretweet"]',
composer: '[data-testid="tweetTextarea_0"]',
postButton: '[data-testid="tweetButton"]',
postButtonInline: '[data-testid="tweetButtonInline"]',
close: '[data-testid="app-bar-close"]',
confirmDiscard: '[data-testid="confirmationSheetConfirm"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const typeIntoComposer = async (el, text) => {
el.focus();
await sleep(150);
const inserted = document.execCommand('insertText', false, text);
if (!inserted) {
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));
el.textContent = text;
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: text, bubbles: true }));
}
await sleep(300);
};
const waitForSelector = async (selector, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(200);
}
return null;
};
// Find the "Quote" item in the repost dropdown menu (locale note: matches the
// English label; on other locales pick the second menu item).
const findQuoteMenuItem = () => {
const items = Array.from(document.querySelectorAll('[role="menuitem"]'));
const byText = items.find((el) => /quote/i.test(el.textContent || ''));
if (byText) return byText;
// Fallback: the repost menu is [Repost, Quote]; take the last item.
return items.length >= 2 ? items[items.length - 1] : null;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { typed: false, posted: false, errors: 0 };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔁 QUOTE TWEET - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
try {
const host = window.location.hostname;
if (!/(^|\.)x\.com$/.test(host) && !/(^|\.)twitter\.com$/.test(host)) {
log.warning('Not on x.com. Open the tweet you want to quote, then re-run.');
return stats;
}
if (!/\/status\/\d+/.test(window.location.pathname)) {
log.warning('Open the target tweet\'s status page first (x.com/<user>/status/<id>), then re-run.');
return stats;
}
if (!CONFIG.text || !CONFIG.text.trim()) {
log.error('CONFIG.text is empty. Set your quote text.');
return stats;
}
// Use the primary (top) article on the status page.
const article = document.querySelector(SELECTORS.article);
if (!article) {
log.error('Tweet article not found on this page.');
stats.errors++;
return stats;
}
if (article.querySelector(SELECTORS.unretweet)) {
log.info('You have already reposted this tweet. A quote will still be posted as a new tweet.');
}
const rtBtn = article.querySelector(SELECTORS.retweet) || article.querySelector(SELECTORS.unretweet);
if (!rtBtn) {
log.error('Repost button not found on the target tweet.');
stats.errors++;
return stats;
}
if (CONFIG.dryRun) {
log.warning('DRY RUN. Nothing posted. Set CONFIG.dryRun = false to post for real.');
log.info(`Would quote with: "${CONFIG.text.slice(0, 80)}${CONFIG.text.length > 80 ? '...' : ''}"`);
return stats;
}
rtBtn.click();
await sleep(900);
const quoteItem = findQuoteMenuItem();
if (!quoteItem) {
log.error('Quote option not found in the repost menu.');
document.body.click();
stats.errors++;
return stats;
}
quoteItem.click();
await sleep(1500);
const composer = await waitForSelector(SELECTORS.composer, 6000);
if (!composer) {
log.error('Quote composer did not open.');
stats.errors++;
return stats;
}
await typeIntoComposer(composer, CONFIG.text);
stats.typed = true;
log.success('Quote text typed.');
await sleep(400);
const postBtn =
document.querySelector(SELECTORS.postButton) ||
document.querySelector(SELECTORS.postButtonInline);
if (!postBtn) {
log.error('Post button not found. Text is typed. Click Post manually.');
stats.errors++;
return stats;
}
if (postBtn.getAttribute('aria-disabled') === 'true' || postBtn.disabled) {
log.error('Post button is disabled (empty or over the character limit).');
stats.errors++;
return stats;
}
postBtn.click();
await sleep(2500);
const stillOpen = document.querySelector(SELECTORS.composer);
const openText = stillOpen ? (stillOpen.textContent || '').trim() : '';
if (!stillOpen || openText.length === 0) {
stats.posted = true;
log.success(`Quote posted: "${CONFIG.text.slice(0, 60)}${CONFIG.text.length > 60 ? '...' : ''}"`);
} else {
log.warning('Composer still open. The quote may not have posted. Check the page.');
stats.errors++;
}
} catch (error) {
log.error(`Error: ${error.message}`);
stats.errors++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 QUOTE TWEET - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ⌨️ Text typed: ${String(stats.typed).padEnd(32)}║
║ ✅ Posted: ${String(stats.posted).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("rate-limiter", function(){
(function rateLimiter() {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ⏱️ RATE LIMITER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
const STORAGE_KEY = 'xactions_ratelimit';
// Default rate limits (conservative/safe)
const defaultLimits = {
follow: {
perHour: 20,
perDay: 100,
cooldownMs: 2000, // Minimum ms between actions
},
unfollow: {
perHour: 20,
perDay: 100,
cooldownMs: 2000,
},
like: {
perHour: 50,
perDay: 200,
cooldownMs: 1000,
},
retweet: {
perHour: 25,
perDay: 100,
cooldownMs: 2000,
},
tweet: {
perHour: 10,
perDay: 50,
cooldownMs: 5000,
},
reply: {
perHour: 20,
perDay: 100,
cooldownMs: 3000,
},
dm: {
perHour: 10,
perDay: 50,
cooldownMs: 30000, // 30 seconds for DMs
},
search: {
perHour: 30,
perDay: 200,
cooldownMs: 2000,
}
};
// Storage helpers
const getData = () => {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : { actions: {}, limits: defaultLimits };
} catch {
return { actions: {}, limits: defaultLimits };
}
};
const saveData = (data) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
};
// Get actions in time window
const getActionsInWindow = (actionType, windowMs) => {
const data = getData();
const actions = data.actions[actionType] || [];
const cutoff = Date.now() - windowMs;
return actions.filter(ts => ts > cutoff);
};
// Record an action
const recordAction = (actionType) => {
const data = getData();
if (!data.actions[actionType]) {
data.actions[actionType] = [];
}
data.actions[actionType].push(Date.now());
// Clean old entries (older than 24 hours)
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
data.actions[actionType] = data.actions[actionType].filter(ts => ts > oneDayAgo);
saveData(data);
};
// Check if action is allowed
const canPerformAction = (actionType) => {
const data = getData();
const limits = data.limits[actionType] || defaultLimits[actionType];
if (!limits) {
return { allowed: true };
}
const hourAgo = 60 * 60 * 1000;
const dayAgo = 24 * 60 * 60 * 1000;
const actionsLastHour = getActionsInWindow(actionType, hourAgo);
const actionsLastDay = getActionsInWindow(actionType, dayAgo);
if (actionsLastHour.length >= limits.perHour) {
const oldestInHour = Math.min(...actionsLastHour);
const waitMs = hourAgo - (Date.now() - oldestInHour);
return {
allowed: false,
reason: 'hourly limit',
waitMs,
current: actionsLastHour.length,
limit: limits.perHour
};
}
if (actionsLastDay.length >= limits.perDay) {
const oldestInDay = Math.min(...actionsLastDay);
const waitMs = dayAgo - (Date.now() - oldestInDay);
return {
allowed: false,
reason: 'daily limit',
waitMs,
current: actionsLastDay.length,
limit: limits.perDay
};
}
// Check cooldown
const lastAction = actionsLastHour[actionsLastHour.length - 1];
if (lastAction) {
const timeSinceLast = Date.now() - lastAction;
if (timeSinceLast < limits.cooldownMs) {
return {
allowed: false,
reason: 'cooldown',
waitMs: limits.cooldownMs - timeSinceLast
};
}
}
return { allowed: true };
};
// Wait for rate limit
const waitForRateLimit = async (actionType) => {
const check = canPerformAction(actionType);
if (check.allowed) return true;
console.log(`⏳ Rate limited (${check.reason}). Waiting ${Math.ceil(check.waitMs / 1000)}s...`);
await new Promise(r => setTimeout(r, check.waitMs + 100));
return true;
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.RateLimit = {
// Check if action is allowed
check: canPerformAction,
// Record an action
record: recordAction,
// Wait for rate limit to clear
wait: waitForRateLimit,
// Perform action with rate limiting
perform: async (actionType, actionFn) => {
const check = canPerformAction(actionType);
if (!check.allowed) {
console.log(`⏳ Rate limited: ${actionType} (${check.reason})`);
await waitForRateLimit(actionType);
}
const result = await actionFn();
recordAction(actionType);
return result;
},
// Get current quotas
quotas: () => {
const data = getData();
const hourAgo = 60 * 60 * 1000;
const dayAgo = 24 * 60 * 60 * 1000;
console.log('');
console.log('═'.repeat(50));
console.log('📊 CURRENT QUOTAS');
console.log('═'.repeat(50));
Object.keys(data.limits).forEach(actionType => {
const limits = data.limits[actionType];
const hourlyCount = getActionsInWindow(actionType, hourAgo).length;
const dailyCount = getActionsInWindow(actionType, dayAgo).length;
const hourPct = Math.round((hourlyCount / limits.perHour) * 100);
const dayPct = Math.round((dailyCount / limits.perDay) * 100);
console.log(`${actionType}:`);
console.log(` Hour: ${hourlyCount}/${limits.perHour} (${hourPct}%)`);
console.log(` Day: ${dailyCount}/${limits.perDay} (${dayPct}%)`);
});
console.log('═'.repeat(50));
console.log('');
},
// Get remaining quota
remaining: (actionType) => {
const data = getData();
const limits = data.limits[actionType] || defaultLimits[actionType];
if (!limits) return { hourly: Infinity, daily: Infinity };
const hourAgo = 60 * 60 * 1000;
const dayAgo = 24 * 60 * 60 * 1000;
const hourlyCount = getActionsInWindow(actionType, hourAgo).length;
const dailyCount = getActionsInWindow(actionType, dayAgo).length;
return {
hourly: Math.max(0, limits.perHour - hourlyCount),
daily: Math.max(0, limits.perDay - dailyCount)
};
},
// Set custom limits
setLimits: (actionType, limits) => {
const data = getData();
data.limits[actionType] = { ...data.limits[actionType], ...limits };
saveData(data);
console.log(`✅ Updated limits for ${actionType}:`, limits);
},
// Get current limits
getLimits: (actionType) => {
const data = getData();
return data.limits[actionType] || defaultLimits[actionType];
},
// Presets
presets: {
// Very conservative
safe: () => {
const data = getData();
data.limits = {
...defaultLimits,
follow: { perHour: 10, perDay: 50, cooldownMs: 5000 },
unfollow: { perHour: 10, perDay: 50, cooldownMs: 5000 },
like: { perHour: 25, perDay: 100, cooldownMs: 2000 },
};
saveData(data);
console.log('✅ Applied SAFE preset (very conservative)');
},
// Default/moderate
moderate: () => {
const data = getData();
data.limits = { ...defaultLimits };
saveData(data);
console.log('✅ Applied MODERATE preset (default)');
},
// Aggressive (use with caution!)
aggressive: () => {
const data = getData();
data.limits = {
...defaultLimits,
follow: { perHour: 40, perDay: 200, cooldownMs: 1000 },
unfollow: { perHour: 40, perDay: 200, cooldownMs: 1000 },
like: { perHour: 100, perDay: 400, cooldownMs: 500 },
};
saveData(data);
console.log('⚠️ Applied AGGRESSIVE preset (use with caution!)');
}
},
// Reset all tracking
reset: () => {
if (confirm('⚠️ Reset all action tracking?')) {
const data = getData();
data.actions = {};
saveData(data);
console.log('✅ Action tracking reset.');
}
},
// Reset to default limits
resetLimits: () => {
if (confirm('⚠️ Reset to default limits?')) {
const data = getData();
data.limits = { ...defaultLimits };
saveData(data);
console.log('✅ Limits reset to defaults.');
}
},
// Time until next action allowed
nextAllowed: (actionType) => {
const check = canPerformAction(actionType);
if (check.allowed) {
console.log(`✅ ${actionType} is allowed now.`);
return 0;
}
const seconds = Math.ceil(check.waitMs / 1000);
console.log(`⏳ ${actionType} allowed in ${seconds}s (${check.reason})`);
return check.waitMs;
},
// Help
help: () => {
console.log('');
console.log('📋 RATE LIMITER COMMANDS:');
console.log('');
console.log(' XActions.RateLimit.check("follow")');
console.log(' XActions.RateLimit.record("follow")');
console.log(' XActions.RateLimit.wait("follow")');
console.log(' XActions.RateLimit.perform("follow", fn)');
console.log(' XActions.RateLimit.quotas()');
console.log(' XActions.RateLimit.remaining("follow")');
console.log(' XActions.RateLimit.nextAllowed("follow")');
console.log(' XActions.RateLimit.setLimits("follow", {perHour: 30})');
console.log(' XActions.RateLimit.getLimits("follow")');
console.log('');
console.log('📦 PRESETS:');
console.log(' XActions.RateLimit.presets.safe()');
console.log(' XActions.RateLimit.presets.moderate()');
console.log(' XActions.RateLimit.presets.aggressive()');
console.log('');
console.log('🔄 RESET:');
console.log(' XActions.RateLimit.reset()');
console.log(' XActions.RateLimit.resetLimits()');
console.log('');
console.log('📊 ACTION TYPES:');
console.log(' follow, unfollow, like, retweet, tweet, reply, dm, search');
console.log('');
}
};
console.log('⏱️ Rate Limiter loaded!');
console.log(' Run XActions.RateLimit.quotas() to see usage.');
console.log(' Run XActions.RateLimit.help() for commands.');
console.log('');
})();
});
register("remove-follower", function(){
(async function removeFollower() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Specific followers to remove (with or without @). Empty = remove from the
// top of your followers list up to maxRemove.
usernames: [
// 'username1',
// 'username2',
],
// Never remove these, even if they appear in usernames or the top-N sweep.
whitelist: [],
// Cap on removals this run (also the top-N count when usernames is empty).
maxRemove: 25,
// SAFETY: preview only. Set to false to actually remove followers.
dryRun: true,
// Extra confirm() prompt before the first live removal.
confirm: true,
// Timing (randomized between min and max, per removal)
minDelay: 2500,
maxDelay: 5000,
// Scrolling to load more followers (top-N mode)
maxScrollRounds: 40,
scrollDelay: 2000,
maxEmptyScrolls: 6
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
userActions: '[data-testid="userActions"]',
menuItem: '[role="menuitem"]',
confirm: '[data-testid="confirmationSheetConfirm"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const cleanHandle = (u) => String(u || '').trim().replace(/^@/, '').toLowerCase();
const getUsername = (cell) => {
const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
if (!link) return null;
const m = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
if (!m) return null;
if (['home', 'explore', 'notifications', 'messages', 'i', 'search', 'settings'].includes(m[1])) return null;
return m[1];
};
const download = (data, filename) => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
console.log(`📥 Downloaded: ${filename}`);
};
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} removed`)
};
// ============================================
// 🎯 STATE
// ============================================
const stats = { removed: 0, scanned: 0, skipped: 0, notFound: 0, errors: 0 };
const removedList = [];
const processed = new Set();
// Stop switch: run window.stopRemoveFollower() to abort after the current row.
let stopped = false;
window.stopRemoveFollower = () => {
stopped = true;
log.warning('Stop requested. Finishing the current follower, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 👋 REMOVE FOLLOWER - XActions ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN - No followers will be removed ║' : '║ 🔴 LIVE MODE - Followers WILL be removed ║'}
╚══════════════════════════════════════════════════════════════╝
`);
log.info('To stop early: window.stopRemoveFollower()');
// Page guard
if (!/\/followers/.test(window.location.pathname)) {
log.error('Not on a followers page. Go to https://x.com/<your-username>/followers and re-run.');
return stats;
}
const targetSet = new Set(CONFIG.usernames.map(cleanHandle).filter(Boolean));
const whiteSet = new Set(CONFIG.whitelist.map(cleanHandle).filter(Boolean));
const targetMode = targetSet.size > 0;
log.info(`Mode: ${targetMode ? `list (${targetSet.size} targets)` : `top ${CONFIG.maxRemove} followers`}`);
log.info(`Max removals: ${CONFIG.maxRemove} | Dry run: ${CONFIG.dryRun}`);
if (!CONFIG.dryRun && CONFIG.confirm) {
const proceed = window.confirm(
`Remove up to ${CONFIG.maxRemove} follower(s) for real? This cannot be auto-undone.`
);
if (!proceed) {
log.warning('Cancelled at confirmation prompt. Nothing removed.');
return stats;
}
}
// ============================================
// 🧹 REMOVE ONE (native remove, else soft-block)
// ============================================
const removeOne = async (cell, username) => {
const moreBtn = cell.querySelector(SELECTORS.userActions) || cell.querySelector('button[aria-label="More"]');
if (!moreBtn) {
log.warning(`No actions (...) button for @${username}.`);
stats.notFound++;
return false;
}
moreBtn.click();
await sleep(700);
const items = Array.from(document.querySelectorAll(SELECTORS.menuItem));
// 1) Preferred: native "Remove this follower" (no block).
const removeItem = items.find(i => /remove\s+this\s+follower|remove\s+follower/i.test(i.textContent));
if (removeItem) {
removeItem.click();
await sleep(600);
const confirm = document.querySelector(SELECTORS.confirm);
if (confirm) { confirm.click(); await sleep(700); }
log.success(`Removed @${username} (native remove).`);
return true;
}
// 2) Fallback: soft-block = block, confirm, then immediately unblock.
const blockItem = items.find(i => /\bblock\b/i.test(i.textContent) && !/unblock/i.test(i.textContent));
if (!blockItem) {
document.body.click();
await sleep(300);
log.warning(`No remove or block option for @${username}.`);
stats.notFound++;
return false;
}
blockItem.click();
await sleep(600);
const blockConfirm = document.querySelector(SELECTORS.confirm);
if (blockConfirm) { blockConfirm.click(); await sleep(800); }
// Re-open the menu on the same row and unblock to complete the soft-block.
const moreBtn2 = cell.querySelector(SELECTORS.userActions) || cell.querySelector('button[aria-label="More"]');
if (moreBtn2) {
moreBtn2.click();
await sleep(700);
const items2 = Array.from(document.querySelectorAll(SELECTORS.menuItem));
const unblock = items2.find(i => /unblock/i.test(i.textContent));
if (unblock) {
unblock.click();
await sleep(500);
const unblockConfirm = document.querySelector(SELECTORS.confirm);
if (unblockConfirm) { unblockConfirm.click(); await sleep(500); }
} else {
document.body.click();
await sleep(300);
}
}
log.success(`Removed @${username} (soft-block).`);
return true;
};
// ============================================
// 🔁 MAIN LOOP
// ============================================
let scrollRounds = 0;
let emptyScrolls = 0;
while (!stopped && stats.removed < CONFIG.maxRemove && scrollRounds < CONFIG.maxScrollRounds && emptyScrolls < CONFIG.maxEmptyScrolls) {
const cells = document.querySelectorAll(SELECTORS.userCell);
let foundNew = false;
for (const cell of cells) {
if (stopped || stats.removed >= CONFIG.maxRemove) break;
const username = getUsername(cell);
if (!username) continue;
const key = username.toLowerCase();
if (processed.has(key)) continue;
processed.add(key);
foundNew = true;
stats.scanned++;
try {
if (whiteSet.has(key)) { stats.skipped++; continue; }
if (targetMode && !targetSet.has(key)) { stats.skipped++; continue; }
if (CONFIG.dryRun) {
log.info(`Would remove: @${username}`);
removedList.push({ username, dryRun: true, timestamp: new Date().toISOString() });
stats.removed++;
continue;
}
const ok = await removeOne(cell, username);
if (ok) {
stats.removed++;
removedList.push({ username, timestamp: new Date().toISOString() });
log.progress(stats.removed, CONFIG.maxRemove);
await randomDelay();
} else {
stats.errors++;
}
} catch (error) {
log.error(`Error on @${username}: ${error.message}`);
stats.errors++;
document.body.click();
await sleep(300);
}
}
if (!foundNew) emptyScrolls++;
else emptyScrolls = 0;
window.scrollTo(0, document.body.scrollHeight);
scrollRounds++;
await sleep(CONFIG.scrollDelay);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 REMOVE FOLLOWER - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ✅ Removed: ${String(stats.removed).padEnd(32)}║
║ 🔍 Scanned: ${String(stats.scanned).padEnd(32)}║
║ ⏭️ Skipped: ${String(stats.skipped).padEnd(32)}║
║ ❓ Not found: ${String(stats.notFound).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
║ 🧪 Dry run: ${String(CONFIG.dryRun).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
if (removedList.length > 0) {
download(
{ stats, dryRun: CONFIG.dryRun, removed: removedList, exportedAt: new Date().toISOString() },
`xactions-removed-followers-${CONFIG.dryRun ? 'preview' : 'results'}-${new Date().toISOString().slice(0, 10)}.json`
);
}
if (CONFIG.dryRun) log.warning('This was a DRY RUN. Set CONFIG.dryRun = false to actually remove.');
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("report-spam", function(){
var CONFIG = {
// Spam detection keywords in bio
spamKeywords: [
'free giveaway',
'click my link',
'dm for cashapp',
'send nudes',
'sexchat',
'hot girls in',
'make $1000 daily',
'guaranteed profits'
],
// Flag accounts with these patterns
detection: {
// Default profile picture
flagDefaultAvatar: true,
// Account following way more than followers
maxFollowingRatio: 100,
// Very new accounts
flagNewAccounts: true, // Can't easily detect from list view
// Bio contains external links
flagExternalLinks: false
},
// Scroll settings
scrollDelay: 1500,
maxScrolls: 20,
// Dry run - just identify, don't report
dryRun: true,
// Max to report per run
maxReports: 10,
// Delay between reports
reportDelay: 5000
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function reportSpam() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 🚨 XActions — Report Spam ║
║ Identify and report spam accounts ║
${CONFIG.dryRun ? '║ ⚠️ DRY RUN MODE - Accounts will NOT be reported ║' : '║ 🔴 LIVE MODE - Accounts WILL be reported ║'}
╚══════════════════════════════════════════════════════════════╝
`);
console.log('⚠️ IMPORTANT: Only report genuine spam/abuse!');
console.log(' False reports can result in action against YOUR account.\n');
const $userCell = '[data-testid="UserCell"]';
console.log('🔍 Looking for spam indicators:');
CONFIG.spamKeywords.forEach(kw => console.log(` • "${kw}"`));
console.log('');
const scanned = new Set();
const spamAccounts = [];
let retries = 0;
let scrollCount = 0;
while (scrollCount < CONFIG.maxScrolls && retries < 3) {
const prevSize = scanned.size;
document.querySelectorAll($userCell).forEach(cell => {
try {
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '')?.split('/')[0];
if (!username || scanned.has(username)) return;
scanned.add(username);
const reasons = [];
let spamScore = 0;
// Check bio
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const bio = (bioEl?.textContent || '').toLowerCase();
CONFIG.spamKeywords.forEach(kw => {
if (bio.includes(kw.toLowerCase())) {
spamScore += 30;
reasons.push(`Bio: "${kw}"`);
}
});
// Check for default avatar
if (CONFIG.detection.flagDefaultAvatar) {
const avatar = cell.querySelector('img[src*="default_profile"]');
if (avatar) {
spamScore += 10;
reasons.push('Default avatar');
}
}
// Check for external links in bio
if (CONFIG.detection.flagExternalLinks && bio.match(/https?:\/\//)) {
spamScore += 5;
reasons.push('External link in bio');
}
if (spamScore >= 30) {
spamAccounts.push({
username,
bio: bioEl?.textContent || '',
spamScore,
reasons,
element: cell
});
}
} catch (e) {
// One malformed cell (missing link, unexpected structure) must never
// abort the whole scan for every other cell in this pass.
}
});
if (scanned.size === prevSize) retries++;
else retries = 0;
console.log(` Scanned: ${scanned.size} | Spam found: ${spamAccounts.length}`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollCount++;
}
console.log(`\n✅ Scan complete!`);
console.log(` Total scanned: ${scanned.size}`);
console.log(` Spam accounts: ${spamAccounts.length}\n`);
if (spamAccounts.length === 0) {
console.log('🎉 No spam accounts detected!');
return;
}
// Sort by spam score
spamAccounts.sort((a, b) => b.spamScore - a.spamScore);
console.log('═'.repeat(60));
console.log('🚨 DETECTED SPAM ACCOUNTS');
console.log('═'.repeat(60));
spamAccounts.forEach((s, i) => {
console.log(`\n${i + 1}. @${s.username} (Score: ${s.spamScore})`);
console.log(` Reasons: ${s.reasons.join(', ')}`);
console.log(` Bio: "${s.bio.slice(0, 80)}${s.bio.length > 80 ? '...' : ''}"`);
console.log(` https://x.com/${s.username}`);
});
if (CONFIG.dryRun) {
console.log('\n' + '═'.repeat(60));
console.log('⚠️ DRY RUN MODE');
console.log('═'.repeat(60));
console.log('\nTo report these accounts:');
console.log('1. Review each account manually');
console.log('2. Only report if they genuinely violate Twitter rules');
console.log('3. Set CONFIG.dryRun = false to enable reporting');
console.log('\nOr report manually by visiting each profile.');
} else {
console.log('\n' + '═'.repeat(60));
console.log('🚨 REPORTING ACCOUNTS');
console.log('═'.repeat(60));
console.log('\n⚠️ Reporting is a serious action. Please review carefully!\n');
const toReport = spamAccounts.slice(0, CONFIG.maxReports);
let reported = 0;
for (const spam of toReport) {
console.log(`\n⏳ Processing @${spam.username}...`);
// The page kept scrolling during the scan above, and X virtualizes
// long lists: a cell captured early can already be detached from the
// DOM by the time we get here. Interacting with a detached node is a
// silent no-op, so skip it explicitly instead of pretending to report.
if (!document.body.contains(spam.element)) {
console.log(` ⚠️ Skipped @${spam.username}: row scrolled out of the DOM. Re-run closer to this account.`);
continue;
}
try {
spam.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
const moreButton = spam.element.querySelector('[data-testid="userActions"]');
if (moreButton) {
moreButton.click();
await sleep(500);
// Find report option
const reportOption = document.querySelector('[data-testid="report"]');
if (reportOption) {
reportOption.click();
await sleep(1000);
// Select "Spam" as reason. Match only short, clickable labels
// (radio/menu items) so a longer descriptive paragraph that
// happens to also contain the word "spam" isn't clicked instead.
const spamOption = Array.from(document.querySelectorAll('span'))
.find(el => {
const text = el.textContent.trim().toLowerCase();
if (!text.includes('spam') || text.length > 40) return false;
return !!el.closest('[role="radio"], [role="menuitem"], [role="button"], label');
});
if (spamOption) {
spamOption.click();
await sleep(500);
// Submit report
const submitBtn = document.querySelector('[data-testid="ChoiceSelectionNextButton"]');
if (submitBtn) {
submitBtn.click();
reported++;
console.log(` ✅ Reported @${spam.username}`);
}
}
// Close dialog
const closeBtn = document.querySelector('[data-testid="app-bar-close"]');
if (closeBtn) closeBtn.click();
}
// Close menu
document.body.click();
}
await sleep(CONFIG.reportDelay);
} catch (e) {
console.log(` ❌ Failed to report @${spam.username}: ${e.message}`);
}
}
console.log(`\n✅ Reported ${reported} accounts`);
}
// Save log
const storageKey = 'xactions_spam_reports';
const log = spamAccounts.map(s => ({
username: s.username,
spamScore: s.spamScore,
reasons: s.reasons,
timestamp: new Date().toISOString()
}));
localStorage.setItem(storageKey, JSON.stringify(log));
console.log('\n💾 Results saved to localStorage');
})();
});
register("schedule-post", function(){
(async function schedulePost() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// The text to schedule.
text: 'Scheduled with XActions 📅',
// When to post, as a local date/time. Format: "YYYY-MM-DDTHH:MM" (24-hour).
// Must be in the future. Example: "2026-08-01T09:00".
scheduleISO: '2026-08-01T09:00',
// Preview only: log what would happen without confirming the schedule.
dryRun: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
composer: '[data-testid="tweetTextarea_0"]',
newTweetButton: 'a[data-testid="SideNav_NewTweet_Button"]',
// The clock icon in the composer toolbar that opens the schedule dialog.
scheduleButton: '[data-testid="scheduleOption"]',
// Legacy fallbacks for the schedule toolbar affordance.
createScheduleAria: '[aria-label="Schedule post"], [aria-label="Schedule"]',
confirmSchedule: '[data-testid="scheduledConfirmationPrimaryAction"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const typeIntoComposer = async (el, text) => {
el.focus();
await sleep(150);
const inserted = document.execCommand('insertText', false, text);
if (!inserted) {
el.dispatchEvent(new InputEvent('beforeinput', { inputType: 'insertText', data: text, bubbles: true, cancelable: true }));
el.textContent = text;
el.dispatchEvent(new InputEvent('input', { inputType: 'insertText', data: text, bubbles: true }));
}
await sleep(300);
};
const waitForSelector = async (selector, timeoutMs = 6000) => {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(200);
}
return null;
};
// Set a native <select> by matching an option's visible text or value, then
// dispatch the change event React listens for.
const setSelectByText = (select, wanted) => {
if (!select) return false;
const target = String(wanted).trim().toLowerCase();
const opts = Array.from(select.options);
const match =
opts.find((o) => o.textContent.trim().toLowerCase() === target) ||
opts.find((o) => o.value.trim().toLowerCase() === target) ||
opts.find((o) => o.textContent.trim().toLowerCase().startsWith(target));
if (!match) return false;
select.value = match.value;
select.dispatchEvent(new Event('input', { bubbles: true }));
select.dispatchEvent(new Event('change', { bubbles: true }));
return true;
};
// Find a select inside the dialog by its aria-label (case-insensitive contains).
const findSelectByLabel = (root, label) => {
const selects = Array.from(root.querySelectorAll('select'));
const l = label.toLowerCase();
return selects.find((s) => {
const aria = (s.getAttribute('aria-label') || '').toLowerCase();
const id = (s.getAttribute('id') || '').toLowerCase();
return aria.includes(l) || id.includes(l);
}) || null;
};
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = { typed: false, dialogOpened: false, scheduled: false, errors: 0, when: CONFIG.scheduleISO };
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📅 SCHEDULE POST - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
try {
const host = window.location.hostname;
if (!/(^|\.)x\.com$/.test(host) && !/(^|\.)twitter\.com$/.test(host)) {
log.warning('Not on x.com. Open x.com/compose/post, then re-run.');
return stats;
}
if (!CONFIG.text || !CONFIG.text.trim()) {
log.error('CONFIG.text is empty. Set the text you want to schedule.');
return stats;
}
const when = new Date(CONFIG.scheduleISO);
if (Number.isNaN(when.getTime())) {
log.error(`CONFIG.scheduleISO is not a valid date/time: "${CONFIG.scheduleISO}". Use "YYYY-MM-DDTHH:MM".`);
return stats;
}
if (when.getTime() <= Date.now()) {
log.error('CONFIG.scheduleISO is in the past. Pick a future date/time.');
return stats;
}
const monthName = MONTHS[when.getMonth()];
const day = String(when.getDate());
const year = String(when.getFullYear());
let hour12 = when.getHours() % 12;
if (hour12 === 0) hour12 = 12;
const minute = String(when.getMinutes()).padStart(2, '0');
const ampm = when.getHours() < 12 ? 'AM' : 'PM';
log.info(`Scheduling for ${monthName} ${day}, ${year} at ${hour12}:${minute} ${ampm} (local time).`);
// Open the composer.
let composer = document.querySelector(SELECTORS.composer);
if (!composer) {
const newBtn = document.querySelector(SELECTORS.newTweetButton);
if (newBtn) {
newBtn.click();
composer = await waitForSelector(SELECTORS.composer, 6000);
}
}
if (!composer) {
log.error('Composer not found. Open x.com/compose/post and re-run.');
stats.errors++;
return stats;
}
await typeIntoComposer(composer, CONFIG.text);
stats.typed = true;
log.success('Text typed into composer.');
if (CONFIG.dryRun) {
log.warning('DRY RUN. The schedule dialog will not be opened or confirmed.');
log.info(`Would schedule: "${CONFIG.text.slice(0, 60)}${CONFIG.text.length > 60 ? '...' : ''}" for ${monthName} ${day} ${year}, ${hour12}:${minute} ${ampm}.`);
return stats;
}
// Open the native Schedule dialog.
const scheduleBtn =
document.querySelector(SELECTORS.scheduleButton) ||
document.querySelector(SELECTORS.createScheduleAria);
if (!scheduleBtn) {
log.error('Schedule button not found in the composer toolbar.');
log.error('X native scheduling may be unavailable on this account. Post it now or schedule manually.');
stats.errors++;
return stats;
}
scheduleBtn.click();
await sleep(1500);
// The dialog is a modal with month/day/year and hour/minute/AM-PM selects.
const dialog = document.querySelector('[role="dialog"]') || document;
const selects = Array.from(dialog.querySelectorAll('select'));
if (selects.length === 0) {
log.error('Schedule dialog selects not found. The scheduler UI did not open as expected.');
log.error('Set the date/time by hand in the dialog, then confirm.');
stats.errors++;
return stats;
}
stats.dialogOpened = true;
// Prefer aria-label matching; fall back to positional order.
const monthSel = findSelectByLabel(dialog, 'month') || selects[0];
const daySel = findSelectByLabel(dialog, 'day') || selects[1];
const yearSel = findSelectByLabel(dialog, 'year') || selects[2];
const hourSel = findSelectByLabel(dialog, 'hour') || selects[3];
const minSel = findSelectByLabel(dialog, 'minute') || selects[4];
const ampmSel = findSelectByLabel(dialog, 'am') || findSelectByLabel(dialog, 'pm') || selects[5];
const setOk = [];
setOk.push(['Month', setSelectByText(monthSel, monthName)]);
setOk.push(['Day', setSelectByText(daySel, day)]);
setOk.push(['Year', setSelectByText(yearSel, year)]);
setOk.push(['Hour', setSelectByText(hourSel, String(hour12))]);
setOk.push(['Minute', setSelectByText(minSel, minute)]);
setOk.push(['AM/PM', setSelectByText(ampmSel, ampm)]);
await sleep(500);
const failed = setOk.filter(([, ok]) => !ok).map(([name]) => name);
if (failed.length > 0) {
log.warning(`Could not set: ${failed.join(', ')}. Adjust those fields by hand before confirming.`);
} else {
log.success('Date and time set in the schedule dialog.');
}
// Confirm the schedule.
const confirm = await waitForSelector(SELECTORS.confirmSchedule, 3000);
if (!confirm) {
log.error('Confirm button not found. Review the dialog and click Confirm manually.');
stats.errors++;
return stats;
}
confirm.click();
await sleep(1500);
stats.scheduled = true;
log.success('Schedule confirmed. Click Post/Schedule in the composer to queue it.');
log.info('Tip: scheduled posts appear under the composer\'s "Unsent posts" tab.');
} catch (error) {
log.error(`Error: ${error.message}`);
stats.errors++;
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCHEDULE POST - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ⌨️ Text typed: ${String(stats.typed).padEnd(32)}║
║ 🗂️ Dialog opened: ${String(stats.dialogOpened).padEnd(32)}║
║ ✅ Scheduled: ${String(stats.scheduled).padEnd(32)}║
║ ❌ Errors: ${String(stats.errors).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
`);
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("scrape-dms", function(){
(async function scrapeDMs() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of messages to collect
maxMessages: 2000,
// Maximum scroll attempts before giving up
maxScrollAttempts: 120,
// Stop after this many scrolls with no new messages (top of history)
noNewThreshold: 6,
// Delay between upward scrolls (ms). Raise if history loads slowly.
scrollDelay: 1200,
// Auto-download the results as files when finished
downloadJSON: true,
downloadCSV: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
// Each message bubble
message: '[data-testid="messageEntry"]',
// Scroller that holds the conversation history
scroller: '[data-testid="DmScrollerContainer"]',
// Conversation heading (used to name the export file)
heading: 'h2[role="heading"], [data-testid="DMConversationHeader"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const getConversationName = () => {
const el = document.querySelector(SELECTORS.heading);
const raw = el ? el.textContent.trim() : '';
return (raw || 'conversation').replace(/[^A-Za-z0-9_]+/g, '_').slice(0, 40) || 'conversation';
};
const getScroller = () => {
return document.querySelector(SELECTORS.scroller) ||
document.querySelector('[data-testid="DMDrawer"] [role="region"]') ||
document.querySelector('[role="main"] [style*="overflow"]') ||
document.querySelector('[role="main"]');
};
// Direction is derived from horizontal position, which is locale-independent:
// outgoing (yours) bubbles sit on the right half, incoming on the left.
const getDirection = (msg, containerRect) => {
try {
const rect = msg.getBoundingClientRect();
if (!rect.width || !containerRect || !containerRect.width) return 'unknown';
const msgCenter = rect.left + rect.width / 2;
const containerCenter = containerRect.left + containerRect.width / 2;
return msgCenter > containerCenter ? 'out' : 'in';
} catch (e) {
return 'unknown';
}
};
const getTimestamp = (msg) => {
// X stores the exact time in a title attribute or a nearby time element
const timeEl = msg.querySelector('time') ||
(msg.closest('[role="listitem"]') || msg.parentElement || msg).querySelector('time');
if (timeEl) return timeEl.getAttribute('datetime') || timeEl.textContent.trim() || '';
const titled = msg.querySelector('[title]');
return titled ? (titled.getAttribute('title') || '') : '';
};
const extractText = (msg) => {
const textEl = msg.querySelector('[data-testid="tweetText"]');
if (textEl && textEl.textContent.trim()) return textEl.textContent.trim();
return (msg.textContent || '').trim();
};
// ============================================
// 🎯 MAIN
// ============================================
let stopped = false;
window.stopScrapeDMs = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 💬 SCRAPE DMS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: must be inside an open conversation (messages/<id>)
if (!/\/messages\/[^/]+/.test(window.location.pathname)) {
log.warning('You do not have a single DM conversation open.');
log.info('Go to https://x.com/messages, click a conversation, then paste this script again.');
return;
}
const conversationName = getConversationName();
log.info(`Conversation: ${conversationName}`);
log.info(`Max messages: ${CONFIG.maxMessages}`);
log.info('Scrolling upward to load history...');
log.info('To stop early: window.stopScrapeDMs()');
console.log('');
const scroller = getScroller();
if (!scroller) {
log.warning('Could not locate the message pane. Try clicking inside the conversation first.');
return;
}
const seen = new Set();
const messages = [];
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && messages.length < CONFIG.maxMessages && scrollAttempts < CONFIG.maxScrollAttempts) {
const containerRect = scroller.getBoundingClientRect();
const bubbles = document.querySelectorAll(SELECTORS.message);
const fresh = [];
bubbles.forEach(msg => {
try {
const text = extractText(msg);
if (!text) return;
const timestamp = getTimestamp(msg);
const direction = getDirection(msg, containerRect);
const key = `${direction}|${text.slice(0, 80)}|${timestamp}`;
if (seen.has(key)) return;
seen.add(key);
fresh.push({
sender: direction === 'out' ? 'you' : (direction === 'in' ? 'them' : 'unknown'),
direction,
text,
timestamp
});
} catch (e) {
// Skip malformed bubbles
}
});
if (fresh.length > 0) {
// Scrolling upward reveals OLDER messages at the top, so prepend them
// (in DOM order) to keep the final array chronological (oldest first).
messages.unshift(...fresh);
log.info(`Collected ${messages.length} messages (+${fresh.length})`);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewThreshold) {
log.warning('No new messages after several scrolls. Reached the top of the history.');
break;
}
}
scroller.scrollTop = Math.max(0, scroller.scrollTop - Math.round(scroller.clientHeight * 0.8) - 200);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
// ============================================
// 📊 SUMMARY
// ============================================
const outCount = messages.filter(m => m.direction === 'out').length;
const inCount = messages.filter(m => m.direction === 'in').length;
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ 📊 SCRAPE DMS - COMPLETE ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log(`💬 Total messages: ${messages.length}`);
console.log(` → Sent (you): ${outCount}`);
console.log(` ← Received: ${inCount}`);
console.log(`📜 Scroll attempts: ${scrollAttempts}`);
if (messages.length === 0) {
log.warning('No messages found. Scroll into the conversation body and try again.');
return { conversation: conversationName, messages: [] };
}
console.log('\n📝 Preview:');
messages.slice(0, 5).forEach(m => {
const arrow = m.direction === 'out' ? '→' : (m.direction === 'in' ? '←' : '·');
console.log(` ${arrow} ${m.text.slice(0, 60)}${m.text.length > 60 ? '...' : ''}`);
});
// ============================================
// 💾 EXPORT
// ============================================
const dateStr = new Date().toISOString().split('T')[0];
const result = {
conversation: conversationName,
exportedAt: new Date().toISOString(),
messageCount: messages.length,
messages
};
console.log('');
if (CONFIG.downloadJSON) {
if (download(JSON.stringify(result, null, 2), `dm_${conversationName}_${dateStr}.json`, 'application/json')) {
log.success('JSON downloaded');
}
}
if (CONFIG.downloadCSV) {
const esc = (v) => `"${String(v).replace(/"/g, '""').replace(/\n/g, ' ')}"`;
const csv = [
['Direction', 'Sender', 'Text', 'Timestamp'].join(','),
...messages.map(m => [esc(m.direction), esc(m.sender), esc(m.text), esc(m.timestamp)].join(','))
].join('\n');
if (download(csv, `dm_${conversationName}_${dateStr}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
}
window.scrapedDMs = result;
console.log('\n💡 Access your data: window.scrapedDMs');
console.log('⚠️ Privacy note: be careful sharing DM exports.');
console.log('✅ Script completed! by nichxbt');
return result;
})();
});
register("scrape-followers", function(){
(async function scrapeFollowers() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Stop after collecting this many followers (safety cap)
maxUsers: 5000,
// How far to scroll each step (pixels)
scrollStep: 1400,
// Delay between scrolls (ms). Raise to 2500+ on a slow connection so rows load.
scrollDelay: 1500,
// Give up after this many consecutive scrolls with no new followers (end of list)
stallLimit: 6,
// Hard cap on scroll attempts, independent of stalls
maxScrollAttempts: 400,
// Also trigger a browser download of the CSV file (JSON always downloads)
autoDownloadCsv: false
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
userDescription: '[data-testid="UserDescription"]',
followsYou: '[data-testid="userFollowIndicator"]',
verified: 'svg[aria-label*="Verified"], [data-testid="icon-verified"]',
profileLink: 'a[href^="/"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const randomDelay = () => sleep(CONFIG.scrollDelay + Math.floor(Math.random() * 500));
const log = {
info: (m) => console.log(`ℹ️ ${m}`),
success: (m) => console.log(`✅ ${m}`),
warning: (m) => console.log(`⚠️ ${m}`),
error: (m) => console.log(`❌ ${m}`),
count: (n) => console.log(`👥 Collected ${n} followers...`)
};
const csvCell = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Download of ${filename} failed: ${e.message}`);
return false;
}
};
// Pull {handle, displayName, bio, verified, followsYou} from one UserCell.
const extractUser = (cell) => {
const anchors = Array.from(cell.querySelectorAll(SELECTORS.profileLink));
// Canonical handle: the bare /username link (avoids /username/photo etc.)
let handle = '';
for (const a of anchors) {
const href = a.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
if (m) { handle = m[1]; break; }
}
if (!handle) {
// Fallback: an anchor whose visible text is the @handle
const at = anchors.find((a) => a.textContent.trim().startsWith('@'));
if (at) handle = at.textContent.trim().slice(1).split(/\s/)[0];
}
if (!handle) return null;
// Display name: profile-link text that is not the @handle line
let displayName = '';
const nameAnchor = anchors.find((a) => {
const t = a.textContent.trim();
return t && !t.startsWith('@');
});
displayName = (nameAnchor && nameAnchor.textContent.trim()) ||
(cell.querySelector('div[dir="ltr"] span')?.textContent.trim()) || '';
let bio = cell.querySelector(SELECTORS.userDescription)?.textContent?.trim() || '';
if (!bio) {
const auto = cell.querySelector('[dir="auto"]:not([data-testid])')?.textContent?.trim();
if (auto && auto.length >= 10 && !auto.startsWith('@')) bio = auto;
}
return {
handle,
displayName,
bio,
verified: !!cell.querySelector(SELECTORS.verified),
followsYou: !!cell.querySelector(SELECTORS.followsYou),
profileUrl: `https://x.com/${handle}`
};
};
// ============================================
// 🛑 STOP SWITCH
// ============================================
let stopped = false;
window.stopScrapeFollowers = () => {
stopped = true;
log.warning('Stop requested. Finishing the current scroll, then exporting what was collected.');
};
// ============================================
// 🚦 PAGE GUARD
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 👥 SCRAPE FOLLOWERS - XActions ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
if (!/\/followers\/?$/.test(window.location.pathname)) {
log.warning('You are not on a followers page.');
log.info('Go to x.com/<user>/followers (for example x.com/nasa/followers), then run this again.');
return;
}
const username = window.location.pathname.split('/')[1] || 'unknown';
log.info(`Scraping followers of @${username}`);
log.info(`Max followers: ${CONFIG.maxUsers}. To stop early: window.stopScrapeFollowers()`);
// ============================================
// 🎯 MAIN LOOP
// ============================================
const users = new Map();
const stats = { collected: 0, verified: 0, followsYou: 0, scrolls: 0, errors: 0 };
let scrollAttempts = 0;
let stall = 0;
while (!stopped && users.size < CONFIG.maxUsers && stall < CONFIG.stallLimit && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = users.size;
const cells = document.querySelectorAll(SELECTORS.userCell);
cells.forEach((cell) => {
try {
const u = extractUser(cell);
if (u && u.handle && !users.has(u.handle)) users.set(u.handle, u);
} catch (e) {
stats.errors++;
}
});
if (users.size > before) {
log.count(users.size);
stall = 0;
} else {
stall++;
}
window.scrollBy(0, CONFIG.scrollStep);
await randomDelay();
scrollAttempts++;
stats.scrolls = scrollAttempts;
}
const list = Array.from(users.values()).slice(0, CONFIG.maxUsers);
stats.collected = list.length;
stats.verified = list.filter((u) => u.verified).length;
stats.followsYou = list.filter((u) => u.followsYou).length;
// ============================================
// 📤 EXPORT
// ============================================
const result = {
profile: username,
profileUrl: `https://x.com/${username}`,
scrapedAt: new Date().toISOString(),
total: list.length,
followers: list
};
const buildCsv = () => {
const header = 'Handle,DisplayName,Bio,Verified,FollowsYou,ProfileURL';
const rows = list.map((u) => [
csvCell('@' + u.handle),
csvCell(u.displayName),
csvCell(u.bio),
u.verified,
u.followsYou,
csvCell(u.profileUrl)
].join(','));
return [header, ...rows].join('\n');
};
const csv = buildCsv();
const dateStr = new Date().toISOString().split('T')[0];
if (list.length === 0) {
log.warning('No followers were collected. The list may be empty, private, or blocked from view.');
} else {
download(JSON.stringify(result, null, 2), `${username}_followers_${dateStr}.json`, 'application/json');
log.success(`Downloaded ${username}_followers_${dateStr}.json`);
if (CONFIG.autoDownloadCsv) {
download(csv, `${username}_followers_${dateStr}.csv`, 'text/csv');
log.success(`Downloaded ${username}_followers_${dateStr}.csv`);
}
}
window.scrapedFollowers = result;
window.scrapeFollowersExportCSV = () => {
download(csv, `${username}_followers_${dateStr}.csv`, 'text/csv');
return csv;
};
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE FOLLOWERS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 👥 Followers collected: ${String(stats.collected).padEnd(30)}║
║ ✔️ Verified: ${String(stats.verified).padEnd(30)}║
║ 🔁 Follow you back: ${String(stats.followsYou).padEnd(30)}║
║ 📜 Scrolls: ${String(stats.scrolls).padEnd(30)}║
║ ❌ Row errors: ${String(stats.errors).padEnd(30)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((u, i) => {
console.log(`${i + 1}. @${u.handle}${u.verified ? ' ✓' : ''} - ${u.displayName}${u.followsYou ? ' (follows you)' : ''}`);
});
if (list.length > 5) console.log(` ... and ${list.length - 5} more`);
console.log('\n📋 CSV preview (full string via window.scrapeFollowersExportCSV()):');
console.log(csv.split('\n').slice(0, 6).join('\n'));
console.log('\n💡 window.scrapedFollowers = full data. window.scrapeFollowersExportCSV() = download + return CSV.');
return result;
})();
});
register("scrape-following", function(){
(async function scrapeFollowing() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Stop after collecting this many accounts (safety cap)
maxUsers: 5000,
// How far to scroll each step (pixels)
scrollStep: 1400,
// Delay between scrolls (ms). Raise to 2500+ on a slow connection so rows load.
scrollDelay: 1500,
// Give up after this many consecutive scrolls with no new accounts (end of list)
stallLimit: 6,
// Hard cap on scroll attempts, independent of stalls
maxScrollAttempts: 400,
// Also trigger a browser download of the CSV file (JSON always downloads)
autoDownloadCsv: false
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
userDescription: '[data-testid="UserDescription"]',
followsYou: '[data-testid="userFollowIndicator"]',
verified: 'svg[aria-label*="Verified"], [data-testid="icon-verified"]',
profileLink: 'a[href^="/"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const randomDelay = () => sleep(CONFIG.scrollDelay + Math.floor(Math.random() * 500));
const log = {
info: (m) => console.log(`ℹ️ ${m}`),
success: (m) => console.log(`✅ ${m}`),
warning: (m) => console.log(`⚠️ ${m}`),
error: (m) => console.log(`❌ ${m}`),
count: (n) => console.log(`👣 Collected ${n} following...`)
};
const csvCell = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Download of ${filename} failed: ${e.message}`);
return false;
}
};
const extractUser = (cell) => {
const anchors = Array.from(cell.querySelectorAll(SELECTORS.profileLink));
let handle = '';
for (const a of anchors) {
const href = a.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
if (m) { handle = m[1]; break; }
}
if (!handle) {
const at = anchors.find((a) => a.textContent.trim().startsWith('@'));
if (at) handle = at.textContent.trim().slice(1).split(/\s/)[0];
}
if (!handle) return null;
let displayName = '';
const nameAnchor = anchors.find((a) => {
const t = a.textContent.trim();
return t && !t.startsWith('@');
});
displayName = (nameAnchor && nameAnchor.textContent.trim()) ||
(cell.querySelector('div[dir="ltr"] span')?.textContent.trim()) || '';
let bio = cell.querySelector(SELECTORS.userDescription)?.textContent?.trim() || '';
if (!bio) {
const auto = cell.querySelector('[dir="auto"]:not([data-testid])')?.textContent?.trim();
if (auto && auto.length >= 10 && !auto.startsWith('@')) bio = auto;
}
return {
handle,
displayName,
bio,
verified: !!cell.querySelector(SELECTORS.verified),
followsYou: !!cell.querySelector(SELECTORS.followsYou),
profileUrl: `https://x.com/${handle}`
};
};
// ============================================
// 🛑 STOP SWITCH
// ============================================
let stopped = false;
window.stopScrapeFollowing = () => {
stopped = true;
log.warning('Stop requested. Finishing the current scroll, then exporting what was collected.');
};
// ============================================
// 🚦 PAGE GUARD
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 👣 SCRAPE FOLLOWING - XActions ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
if (!/\/following\/?$/.test(window.location.pathname)) {
log.warning('You are not on a following page.');
log.info('Go to x.com/<user>/following (for example x.com/nasa/following), then run this again.');
return;
}
const username = window.location.pathname.split('/')[1] || 'unknown';
log.info(`Scraping following list of @${username}`);
log.info(`Max accounts: ${CONFIG.maxUsers}. To stop early: window.stopScrapeFollowing()`);
// ============================================
// 🎯 MAIN LOOP
// ============================================
const users = new Map();
const stats = { collected: 0, verified: 0, mutuals: 0, scrolls: 0, errors: 0 };
let scrollAttempts = 0;
let stall = 0;
while (!stopped && users.size < CONFIG.maxUsers && stall < CONFIG.stallLimit && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = users.size;
const cells = document.querySelectorAll(SELECTORS.userCell);
cells.forEach((cell) => {
try {
const u = extractUser(cell);
if (u && u.handle && !users.has(u.handle)) users.set(u.handle, u);
} catch (e) {
stats.errors++;
}
});
if (users.size > before) {
log.count(users.size);
stall = 0;
} else {
stall++;
}
window.scrollBy(0, CONFIG.scrollStep);
await randomDelay();
scrollAttempts++;
stats.scrolls = scrollAttempts;
}
const list = Array.from(users.values()).slice(0, CONFIG.maxUsers);
stats.collected = list.length;
stats.verified = list.filter((u) => u.verified).length;
stats.mutuals = list.filter((u) => u.followsYou).length;
// ============================================
// 📤 EXPORT
// ============================================
const result = {
profile: username,
profileUrl: `https://x.com/${username}`,
scrapedAt: new Date().toISOString(),
total: list.length,
mutuals: stats.mutuals,
following: list
};
const buildCsv = () => {
const header = 'Handle,DisplayName,Bio,Verified,FollowsYou,ProfileURL';
const rows = list.map((u) => [
csvCell('@' + u.handle),
csvCell(u.displayName),
csvCell(u.bio),
u.verified,
u.followsYou,
csvCell(u.profileUrl)
].join(','));
return [header, ...rows].join('\n');
};
const csv = buildCsv();
const dateStr = new Date().toISOString().split('T')[0];
if (list.length === 0) {
log.warning('No accounts were collected. The list may be empty, private, or blocked from view.');
} else {
download(JSON.stringify(result, null, 2), `${username}_following_${dateStr}.json`, 'application/json');
log.success(`Downloaded ${username}_following_${dateStr}.json`);
if (CONFIG.autoDownloadCsv) {
download(csv, `${username}_following_${dateStr}.csv`, 'text/csv');
log.success(`Downloaded ${username}_following_${dateStr}.csv`);
}
}
window.scrapedFollowing = result;
window.scrapeFollowingExportCSV = () => {
download(csv, `${username}_following_${dateStr}.csv`, 'text/csv');
return csv;
};
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE FOLLOWING - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 👣 Accounts collected: ${String(stats.collected).padEnd(30)}║
║ ✔️ Verified: ${String(stats.verified).padEnd(30)}║
║ 🤝 Mutuals (follow you):${String(stats.mutuals).padEnd(30)}║
║ 📜 Scrolls: ${String(stats.scrolls).padEnd(30)}║
║ ❌ Row errors: ${String(stats.errors).padEnd(30)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((u, i) => {
console.log(`${i + 1}. @${u.handle}${u.verified ? ' ✓' : ''} - ${u.displayName}${u.followsYou ? ' (follows you)' : ''}`);
});
if (list.length > 5) console.log(` ... and ${list.length - 5} more`);
console.log('\n📋 CSV preview (full string via window.scrapeFollowingExportCSV()):');
console.log(csv.split('\n').slice(0, 6).join('\n'));
console.log('\n💡 window.scrapedFollowing = full data. window.scrapeFollowingExportCSV() = download + return CSV.');
return result;
})();
});
register("scrape-hashtag", function(){
(async function scrapeHashtag() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Hashtag to scrape WITHOUT the # (e.g. "solana"). Leave "" to scrape the
// hashtag already open in the current search results.
hashtag: '',
// Maximum number of posts to collect before stopping
maxPosts: 300,
// Include reply posts in the results (false = original posts only)
includeReplies: true,
// Delay between scrolls (ms). Raise if posts load slowly.
scrollDelay: 1800,
// Give up after this many scrolls that add no new posts
noNewPostsThreshold: 6,
// Hard cap on scroll attempts so the loop can never run forever
maxScrollAttempts: 400,
// Auto-download a CSV file too (JSON always downloads)
downloadCsv: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userName: '[data-testid="User-Name"]',
actionGroup: '[role="group"]',
photo: '[data-testid="tweetPhoto"] img',
video: 'video'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (n, total) => console.log(`📊 Collected ${n}/${total} posts...`)
};
const countFromLabel = (label) => {
if (!label) return 0;
const raw = (label.match(/[\d.,]+\s*[KMB]?/i) || ['0'])[0].trim().toUpperCase();
const num = parseFloat(raw.replace(/,/g, '')) || 0;
if (raw.includes('K')) return Math.round(num * 1e3);
if (raw.includes('M')) return Math.round(num * 1e6);
if (raw.includes('B')) return Math.round(num * 1e9);
return Math.round(num);
};
const getCounts = (article) => {
const counts = { replies: 0, reposts: 0, likes: 0, views: 0 };
const group = article.querySelector(SELECTORS.actionGroup);
const nodes = group ? group.querySelectorAll('[aria-label]') : [];
nodes.forEach((node) => {
const label = (node.getAttribute('aria-label') || '').toLowerCase();
const value = countFromLabel(label);
if (label.includes('repl')) counts.replies = value;
else if (label.includes('repost') || label.includes('retweet')) counts.reposts = value;
else if (label.includes('like')) counts.likes = value;
else if (label.includes('view')) counts.views = value;
});
return counts;
};
const toOriginalImage = (url) => {
if (!url) return url;
return url.replace(/&name=[^&]*/i, '&name=orig').replace(/\?name=[^&]*/i, '?name=orig');
};
const getMediaUrls = (article) => {
const urls = new Set();
article.querySelectorAll(SELECTORS.photo).forEach((img) => {
if (img.src) urls.add(toOriginalImage(img.src));
});
article.querySelectorAll(SELECTORS.video).forEach((v) => {
if (v.src) urls.add(v.src);
else if (v.poster) urls.add(v.poster);
});
return Array.from(urls);
};
const isReply = (article) => {
if (article.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(article.querySelectorAll('div[dir]')).some((el) =>
(el.textContent || '').startsWith('Replying to'));
};
const extractHashtags = (text) => text.match(/#\w+/g) || [];
const extractPost = (article) => {
const timeEl = article.querySelector('time');
const permalink = timeEl ? timeEl.closest('a[href*="/status/"]') : null;
const anchor = permalink || article.querySelector('a[href*="/status/"]');
const url = anchor ? anchor.href : '';
const id = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
if (!id) return null;
const nameBlock = article.querySelector(SELECTORS.userName);
const nameText = nameBlock ? nameBlock.textContent : '';
const handle = (nameText.match(/@(\w+)/) || [, ''])[1];
const name = nameText.split('@')[0].trim();
const textEl = article.querySelector(SELECTORS.tweetText);
const text = textEl ? textEl.textContent : '';
const counts = getCounts(article);
return {
id,
url,
author: { handle, name },
text,
likes: counts.likes,
reposts: counts.reposts,
replies: counts.replies,
views: counts.views,
timestamp: timeEl ? timeEl.getAttribute('datetime') : null,
mediaUrls: getMediaUrls(article),
hashtags: extractHashtags(text),
isReply: isReply(article),
scrapedAt: new Date().toISOString()
};
};
const downloadFile = (content, filename, mime) => {
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
return true;
} catch (e) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const csvCell = (value) => `"${String(value == null ? '' : value).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const toCSV = (posts) => {
const headers = ['id', 'handle', 'name', 'text', 'likes', 'reposts', 'replies', 'views', 'timestamp', 'hashtags', 'mediaUrls', 'url'];
const rows = posts.map((p) => [
p.id,
'@' + p.author.handle,
p.author.name,
p.text,
p.likes,
p.reposts,
p.replies,
p.views,
p.timestamp || '',
p.hashtags.join(' '),
p.mediaUrls.join(' '),
p.url
].map(csvCell).join(','));
return [headers.join(','), ...rows].join('\n');
};
// Resolve the target hashtag from CONFIG or the current URL.
const currentHashtag = () => {
const q = new URLSearchParams(window.location.search).get('q') || '';
const fromSearch = q.match(/#(\w+)/) || q.match(/%23(\w+)/i);
if (fromSearch) return fromSearch[1];
const fromPath = window.location.pathname.match(/hashtag\/(\w+)/);
return fromPath ? fromPath[1] : '';
};
// ============================================
// 🎯 MAIN
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ #️⃣ SCRAPE HASHTAG - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
const configTag = CONFIG.hashtag.replace(/^#/, '').trim();
const openTag = currentHashtag();
// If a hashtag is configured and we are not already on its results, navigate
// there once. Re-run the script after the page loads to collect.
if (configTag && configTag.toLowerCase() !== openTag.toLowerCase()) {
const target = `https://x.com/search?q=%23${encodeURIComponent(configTag)}&f=live`;
log.warning(`Not on the #${configTag} results yet. Opening them now.`);
log.info('When the results finish loading, paste this script again to collect.');
window.location.href = target;
return;
}
const hashtag = configTag || openTag;
if (!hashtag) {
log.warning('No hashtag detected.');
log.info('Set CONFIG.hashtag = "yourtag", or go to x.com/search?q=%23yourtag&f=live, then run again.');
return;
}
const posts = new Map();
// Stop switch: run window.stopScrapeHashtag() to end the scroll loop.
let stopped = false;
window.stopScrapeHashtag = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
log.info(`Hashtag: #${hashtag}`);
log.info(`Max posts: ${CONFIG.maxPosts} | Include replies: ${CONFIG.includeReplies}`);
log.info('To stop early: window.stopScrapeHashtag()');
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && posts.size < CONFIG.maxPosts && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = posts.size;
const articles = document.querySelectorAll(SELECTORS.tweet);
articles.forEach((article) => {
try {
const post = extractPost(article);
if (!post || posts.has(post.id)) return;
if (!CONFIG.includeReplies && post.isReply) return;
posts.set(post.id, post);
} catch (e) {
log.error(`Skipped a post: ${e.message}`);
}
});
if (posts.size > before) {
log.progress(posts.size, CONFIG.maxPosts);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewPostsThreshold) {
log.warning('No new posts after several scrolls. Reached the end of the results.');
break;
}
}
if (posts.size >= CONFIG.maxPosts) break;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
const list = Array.from(posts.values()).slice(0, CONFIG.maxPosts);
// ============================================
// 📊 SUMMARY + EXPORT
// ============================================
if (list.length === 0) {
log.warning('No posts were found for this hashtag. The results may be empty, or the page had not loaded yet.');
return;
}
const totalLikes = list.reduce((s, p) => s + p.likes, 0);
const coTags = {};
list.forEach((p) => p.hashtags.forEach((h) => {
const k = h.toLowerCase();
coTags[k] = (coTags[k] || 0) + 1;
}));
const topCoTags = Object.entries(coTags).sort((a, b) => b[1] - a[1]).slice(0, 8);
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE HASHTAG - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ #️⃣ Hashtag: ${String('#' + hashtag).slice(0, 36).padEnd(37)}║
║ 📝 Posts: ${String(list.length).padEnd(37)}║
║ ❤️ Total likes: ${String(totalLikes.toLocaleString()).padEnd(37)}║
╚══════════════════════════════════════════════════════════╝
`);
if (topCoTags.length > 0) {
console.log('🏷️ Co-occurring hashtags:');
topCoTags.forEach(([tag, count], i) => console.log(` ${i + 1}. ${tag} (${count})`));
}
list.slice(0, 5).forEach((p, i) => {
console.log(`${i + 1}. @${p.author.handle} (${p.likes} ❤️): "${p.text.slice(0, 60)}"`);
});
const stamp = new Date().toISOString().split('T')[0];
const result = { hashtag, scrapedAt: new Date().toISOString(), count: list.length, posts: list };
if (downloadFile(JSON.stringify(result, null, 2), `hashtag_${hashtag}_${stamp}.json`, 'application/json')) {
log.success('JSON downloaded');
}
if (CONFIG.downloadCsv && downloadFile(toCSV(list), `hashtag_${hashtag}_${stamp}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
window.scrapedHashtag = result;
window.exportCSV = () => toCSV(list);
console.log('');
log.info('window.scrapedHashtag holds the full data. window.exportCSV() returns the CSV string.');
log.success('Done! by nichxbt');
return result;
})();
});
register("scrape-likers", function(){
(async function scrapeLikers() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Stop after collecting this many likers (safety cap)
maxUsers: 5000,
// How far to scroll each step (pixels)
scrollStep: 1400,
// Delay between scrolls (ms). Raise to 2500+ on a slow connection so rows load.
scrollDelay: 1500,
// Give up after this many consecutive scrolls with no new likers (end of list)
stallLimit: 6,
// Hard cap on scroll attempts, independent of stalls
maxScrollAttempts: 400,
// Also trigger a browser download of the CSV file (JSON always downloads)
autoDownloadCsv: false
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
userDescription: '[data-testid="UserDescription"]',
followsYou: '[data-testid="userFollowIndicator"]',
verified: 'svg[aria-label*="Verified"], [data-testid="icon-verified"]',
profileLink: 'a[href^="/"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const randomDelay = () => sleep(CONFIG.scrollDelay + Math.floor(Math.random() * 500));
const log = {
info: (m) => console.log(`ℹ️ ${m}`),
success: (m) => console.log(`✅ ${m}`),
warning: (m) => console.log(`⚠️ ${m}`),
error: (m) => console.log(`❌ ${m}`),
count: (n) => console.log(`❤️ Collected ${n} likers...`)
};
const csvCell = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Download of ${filename} failed: ${e.message}`);
return false;
}
};
const extractUser = (cell) => {
const anchors = Array.from(cell.querySelectorAll(SELECTORS.profileLink));
let handle = '';
for (const a of anchors) {
const href = a.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
if (m) { handle = m[1]; break; }
}
if (!handle) {
const at = anchors.find((a) => a.textContent.trim().startsWith('@'));
if (at) handle = at.textContent.trim().slice(1).split(/\s/)[0];
}
if (!handle) return null;
let displayName = '';
const nameAnchor = anchors.find((a) => {
const t = a.textContent.trim();
return t && !t.startsWith('@');
});
displayName = (nameAnchor && nameAnchor.textContent.trim()) ||
(cell.querySelector('div[dir="ltr"] span')?.textContent.trim()) || '';
let bio = cell.querySelector(SELECTORS.userDescription)?.textContent?.trim() || '';
if (!bio) {
const auto = cell.querySelector('[dir="auto"]:not([data-testid])')?.textContent?.trim();
if (auto && auto.length >= 10 && !auto.startsWith('@')) bio = auto;
}
return {
handle,
displayName,
bio,
verified: !!cell.querySelector(SELECTORS.verified),
followsYou: !!cell.querySelector(SELECTORS.followsYou),
profileUrl: `https://x.com/${handle}`
};
};
const getTweetId = () => (window.location.pathname.match(/status\/(\d+)/) || [])[1] || null;
// ============================================
// 🛑 STOP SWITCH
// ============================================
let stopped = false;
window.stopScrapeLikers = () => {
stopped = true;
log.warning('Stop requested. Finishing the current scroll, then exporting what was collected.');
};
// ============================================
// 🚦 PAGE GUARD
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ ❤️ SCRAPE LIKERS - XActions ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
const tweetId = getTweetId();
if (!/\/likes\/?$/.test(window.location.pathname) || !tweetId) {
log.warning('You are not on a post Likes page.');
log.info('Open a post, click its likes count, and land on x.com/<user>/status/<id>/likes, then run this again.');
return;
}
log.info(`Scraping likers of post ${tweetId}`);
log.info(`Max likers: ${CONFIG.maxUsers}. To stop early: window.stopScrapeLikers()`);
// ============================================
// 🎯 MAIN LOOP
// ============================================
const users = new Map();
const stats = { collected: 0, verified: 0, followsYou: 0, scrolls: 0, errors: 0 };
let scrollAttempts = 0;
let stall = 0;
while (!stopped && users.size < CONFIG.maxUsers && stall < CONFIG.stallLimit && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = users.size;
const cells = document.querySelectorAll(SELECTORS.userCell);
cells.forEach((cell) => {
try {
const u = extractUser(cell);
if (u && u.handle && !users.has(u.handle)) users.set(u.handle, u);
} catch (e) {
stats.errors++;
}
});
if (users.size > before) {
log.count(users.size);
stall = 0;
} else {
stall++;
}
window.scrollBy(0, CONFIG.scrollStep);
await randomDelay();
scrollAttempts++;
stats.scrolls = scrollAttempts;
}
const list = Array.from(users.values()).slice(0, CONFIG.maxUsers);
stats.collected = list.length;
stats.verified = list.filter((u) => u.verified).length;
stats.followsYou = list.filter((u) => u.followsYou).length;
// ============================================
// 📤 EXPORT
// ============================================
const result = {
tweetId,
tweetUrl: `https://x.com${window.location.pathname.replace(/\/likes\/?$/, '')}`,
scrapedAt: new Date().toISOString(),
total: list.length,
likers: list
};
const buildCsv = () => {
const header = 'Handle,DisplayName,Bio,Verified,FollowsYou,ProfileURL';
const rows = list.map((u) => [
csvCell('@' + u.handle),
csvCell(u.displayName),
csvCell(u.bio),
u.verified,
u.followsYou,
csvCell(u.profileUrl)
].join(','));
return [header, ...rows].join('\n');
};
const csv = buildCsv();
const dateStr = new Date().toISOString().split('T')[0];
if (list.length === 0) {
log.warning('No likers were collected. The post may have zero likes, or the list is hidden.');
} else {
download(JSON.stringify(result, null, 2), `tweet_${tweetId}_likers_${dateStr}.json`, 'application/json');
log.success(`Downloaded tweet_${tweetId}_likers_${dateStr}.json`);
if (CONFIG.autoDownloadCsv) {
download(csv, `tweet_${tweetId}_likers_${dateStr}.csv`, 'text/csv');
log.success(`Downloaded tweet_${tweetId}_likers_${dateStr}.csv`);
}
}
window.scrapedLikers = result;
window.scrapeLikersExportCSV = () => {
download(csv, `tweet_${tweetId}_likers_${dateStr}.csv`, 'text/csv');
return csv;
};
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE LIKERS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ ❤️ Likers collected: ${String(stats.collected).padEnd(30)}║
║ ✔️ Verified: ${String(stats.verified).padEnd(30)}║
║ 🔁 Follow you: ${String(stats.followsYou).padEnd(30)}║
║ 📜 Scrolls: ${String(stats.scrolls).padEnd(30)}║
║ ❌ Row errors: ${String(stats.errors).padEnd(30)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((u, i) => {
console.log(`${i + 1}. @${u.handle}${u.verified ? ' ✓' : ''} - ${u.displayName}`);
});
if (list.length > 5) console.log(` ... and ${list.length - 5} more`);
console.log('\n📋 CSV preview (full string via window.scrapeLikersExportCSV()):');
console.log(csv.split('\n').slice(0, 6).join('\n'));
console.log('\n💡 window.scrapedLikers = full data. window.scrapeLikersExportCSV() = download + return CSV.');
return result;
})();
});
register("scrape-list", function(){
(async function scrapeList() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// What to scrape: 'timeline' (the List's posts) or 'members' (the accounts
// on the List). Match this to the page you are on. Leave 'auto' to detect
// from the URL (a /members path selects members, otherwise timeline).
mode: 'auto',
// Maximum posts (timeline) or members to collect before stopping
maxItems: 500,
// Include reply posts in timeline mode (false = original posts only)
includeReplies: true,
// Delay between scrolls (ms). Raise if content loads slowly.
scrollDelay: 1800,
// Give up after this many scrolls that add nothing new
noNewItemsThreshold: 6,
// Hard cap on scroll attempts so the loop can never run forever
maxScrollAttempts: 500,
// Auto-download a CSV file too (JSON always downloads)
downloadCsv: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userName: '[data-testid="User-Name"]',
actionGroup: '[role="group"]',
photo: '[data-testid="tweetPhoto"] img',
video: 'video',
userCell: '[data-testid="UserCell"]',
userDescription: '[data-testid="UserDescription"]',
heading: 'h2[role="heading"], [data-testid="primaryColumn"] h1'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (n, total, noun) => console.log(`📊 Collected ${n}/${total} ${noun}...`)
};
const countFromLabel = (label) => {
if (!label) return 0;
const raw = (label.match(/[\d.,]+\s*[KMB]?/i) || ['0'])[0].trim().toUpperCase();
const num = parseFloat(raw.replace(/,/g, '')) || 0;
if (raw.includes('K')) return Math.round(num * 1e3);
if (raw.includes('M')) return Math.round(num * 1e6);
if (raw.includes('B')) return Math.round(num * 1e9);
return Math.round(num);
};
const getCounts = (article) => {
const counts = { replies: 0, reposts: 0, likes: 0, views: 0 };
const group = article.querySelector(SELECTORS.actionGroup);
const nodes = group ? group.querySelectorAll('[aria-label]') : [];
nodes.forEach((node) => {
const label = (node.getAttribute('aria-label') || '').toLowerCase();
const value = countFromLabel(label);
if (label.includes('repl')) counts.replies = value;
else if (label.includes('repost') || label.includes('retweet')) counts.reposts = value;
else if (label.includes('like')) counts.likes = value;
else if (label.includes('view')) counts.views = value;
});
return counts;
};
const toOriginalImage = (url) => {
if (!url) return url;
return url.replace(/&name=[^&]*/i, '&name=orig').replace(/\?name=[^&]*/i, '?name=orig');
};
const getMediaUrls = (article) => {
const urls = new Set();
article.querySelectorAll(SELECTORS.photo).forEach((img) => {
if (img.src) urls.add(toOriginalImage(img.src));
});
article.querySelectorAll(SELECTORS.video).forEach((v) => {
if (v.src) urls.add(v.src);
else if (v.poster) urls.add(v.poster);
});
return Array.from(urls);
};
const isReply = (article) => {
if (article.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(article.querySelectorAll('div[dir]')).some((el) =>
(el.textContent || '').startsWith('Replying to'));
};
const extractPost = (article) => {
const timeEl = article.querySelector('time');
const permalink = timeEl ? timeEl.closest('a[href*="/status/"]') : null;
const anchor = permalink || article.querySelector('a[href*="/status/"]');
const url = anchor ? anchor.href : '';
const id = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
if (!id) return null;
const nameBlock = article.querySelector(SELECTORS.userName);
const nameText = nameBlock ? nameBlock.textContent : '';
const handle = (nameText.match(/@(\w+)/) || [, ''])[1];
const name = nameText.split('@')[0].trim();
const textEl = article.querySelector(SELECTORS.tweetText);
const text = textEl ? textEl.textContent : '';
const counts = getCounts(article);
return {
id,
url,
author: { handle, name },
text,
likes: counts.likes,
reposts: counts.reposts,
replies: counts.replies,
views: counts.views,
timestamp: timeEl ? timeEl.getAttribute('datetime') : null,
mediaUrls: getMediaUrls(article),
isReply: isReply(article),
scrapedAt: new Date().toISOString()
};
};
const extractMember = (cell) => {
const handleAnchor = cell.querySelector('a[href^="/"]');
const href = handleAnchor ? handleAnchor.getAttribute('href') : '';
const handle = (href || '').replace(/^\//, '').split('/')[0];
if (!handle) return null;
const nameBlock = cell.querySelector(SELECTORS.userName) || cell.querySelector('a[href^="/"]');
const nameText = nameBlock ? nameBlock.textContent : '';
const name = nameText.split('@')[0].trim();
const bioEl = cell.querySelector(SELECTORS.userDescription);
const verified = !!cell.querySelector('svg[aria-label*="Verified"]');
return {
handle,
name,
bio: bioEl ? bioEl.textContent : '',
verified,
profileUrl: `https://x.com/${handle}`,
scrapedAt: new Date().toISOString()
};
};
const downloadFile = (content, filename, mime) => {
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
return true;
} catch (e) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const csvCell = (value) => `"${String(value == null ? '' : value).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const postsToCSV = (posts) => {
const headers = ['id', 'handle', 'name', 'text', 'likes', 'reposts', 'replies', 'views', 'timestamp', 'mediaUrls', 'url'];
const rows = posts.map((p) => [
p.id, '@' + p.author.handle, p.author.name, p.text, p.likes, p.reposts,
p.replies, p.views, p.timestamp || '', p.mediaUrls.join(' '), p.url
].map(csvCell).join(','));
return [headers.join(','), ...rows].join('\n');
};
const membersToCSV = (members) => {
const headers = ['handle', 'name', 'bio', 'verified', 'profileUrl'];
const rows = members.map((m) => [
'@' + m.handle, m.name, m.bio, m.verified, m.profileUrl
].map(csvCell).join(','));
return [headers.join(','), ...rows].join('\n');
};
const getListId = () => {
const match = window.location.pathname.match(/lists\/(\d+)/);
return match ? match[1] : null;
};
const getListName = () => {
const header = document.querySelector(SELECTORS.heading);
return header ? header.textContent.trim() : 'list';
};
// ============================================
// 🎯 MAIN
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📋 SCRAPE LIST - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn and return, never hard-redirect.
if (!/\/lists\/\d+/.test(window.location.pathname)) {
log.warning('You are not on an X List page.');
log.info('Go to x.com/i/lists/<listId> (timeline) or x.com/i/lists/<listId>/members, then run again.');
return;
}
const mode = CONFIG.mode === 'auto'
? (/\/members\b/.test(window.location.pathname) ? 'members' : 'timeline')
: CONFIG.mode;
if (mode !== 'timeline' && mode !== 'members') {
log.error(`Invalid CONFIG.mode "${CONFIG.mode}". Use 'timeline', 'members', or 'auto'.`);
return;
}
const listId = getListId();
const listName = getListName();
const items = new Map();
// Stop switch: run window.stopScrapeList() to end the scroll loop.
let stopped = false;
window.stopScrapeList = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
const noun = mode === 'members' ? 'members' : 'posts';
log.info(`List: "${listName}" (id ${listId})`);
log.info(`Mode: ${mode} | Max ${noun}: ${CONFIG.maxItems}`);
if (mode === 'timeline') log.info(`Include replies: ${CONFIG.includeReplies}`);
log.info('To stop early: window.stopScrapeList()');
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && items.size < CONFIG.maxItems && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = items.size;
if (mode === 'members') {
document.querySelectorAll(SELECTORS.userCell).forEach((cell) => {
try {
const member = extractMember(cell);
if (member && !items.has(member.handle)) items.set(member.handle, member);
} catch (e) {
log.error(`Skipped a member: ${e.message}`);
}
});
} else {
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
try {
const post = extractPost(article);
if (!post || items.has(post.id)) return;
if (!CONFIG.includeReplies && post.isReply) return;
items.set(post.id, post);
} catch (e) {
log.error(`Skipped a post: ${e.message}`);
}
});
}
if (items.size > before) {
log.progress(items.size, CONFIG.maxItems, noun);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewItemsThreshold) {
log.warning(`No new ${noun} after several scrolls. Reached the end.`);
break;
}
}
if (items.size >= CONFIG.maxItems) break;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
const list = Array.from(items.values()).slice(0, CONFIG.maxItems);
// ============================================
// 📊 SUMMARY + EXPORT
// ============================================
if (list.length === 0) {
log.warning(`No ${noun} were found. The List may be empty, or the page had not loaded yet.`);
return;
}
const safeName = listName.replace(/[^a-z0-9]/gi, '_').slice(0, 30) || 'list';
const stamp = new Date().toISOString().split('T')[0];
if (mode === 'members') {
const verified = list.filter((m) => m.verified).length;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE LIST (MEMBERS) - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 📋 List: ${String(listName).slice(0, 36).padEnd(37)}║
║ 👥 Members: ${String(list.length).padEnd(37)}║
║ ✓ Verified: ${String(verified).padEnd(37)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((m, i) => console.log(`${i + 1}. @${m.handle}${m.verified ? ' ✓' : ''} - ${m.name}`));
const result = { listId, listName, mode, scrapedAt: new Date().toISOString(), count: list.length, members: list };
if (downloadFile(JSON.stringify(result, null, 2), `list_${safeName}_members_${stamp}.json`, 'application/json')) log.success('JSON downloaded');
if (CONFIG.downloadCsv && downloadFile(membersToCSV(list), `list_${safeName}_members_${stamp}.csv`, 'text/csv')) log.success('CSV downloaded');
window.scrapedList = result;
window.exportCSV = () => membersToCSV(list);
} else {
const totalLikes = list.reduce((s, p) => s + p.likes, 0);
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE LIST (TIMELINE) - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 📋 List: ${String(listName).slice(0, 36).padEnd(37)}║
║ 📝 Posts: ${String(list.length).padEnd(37)}║
║ ❤️ Total likes: ${String(totalLikes.toLocaleString()).padEnd(37)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((p, i) => console.log(`${i + 1}. @${p.author.handle} (${p.likes} ❤️): "${p.text.slice(0, 60)}"`));
const result = { listId, listName, mode, scrapedAt: new Date().toISOString(), count: list.length, posts: list };
if (downloadFile(JSON.stringify(result, null, 2), `list_${safeName}_timeline_${stamp}.json`, 'application/json')) log.success('JSON downloaded');
if (CONFIG.downloadCsv && downloadFile(postsToCSV(list), `list_${safeName}_timeline_${stamp}.csv`, 'text/csv')) log.success('CSV downloaded');
window.scrapedList = result;
window.exportCSV = () => postsToCSV(list);
}
console.log('');
log.info('window.scrapedList holds the full data. window.exportCSV() returns the CSV string.');
log.success('Done! by nichxbt');
return window.scrapedList;
})();
});
register("scrape-media", function(){
(async function scrapeMedia() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of media items to collect before stopping
maxItems: 500,
// Upgrade image URLs to original resolution (strip the &name= size param)
highestRes: true,
// Which media types to keep
includeImages: true,
includeVideos: true,
includeGifs: true,
// Delay between scrolls (ms). Raise if the grid loads slowly.
scrollDelay: 1800,
// Give up after this many scrolls that add nothing new
noNewItemsThreshold: 6,
// Hard cap on scroll attempts so the loop can never run forever
maxScrollAttempts: 400,
// Auto-download a CSV file and a plain-text URL list too (JSON always downloads)
downloadCsv: true,
downloadUrlList: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
userName: '[data-testid="User-Name"]',
tweetPhoto: '[data-testid="tweetPhoto"] img',
video: 'video',
gifThumb: 'img[src*="tweet_video_thumb"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (n, total) => console.log(`📊 Collected ${n}/${total} media items...`)
};
// Rewrite the &name= / ?name= size param to the original resolution.
const toOriginalImage = (url) => {
if (!url) return url;
if (/[?&]name=/i.test(url)) {
return url.replace(/&name=[^&]*/i, '&name=orig').replace(/\?name=[^&]*/i, '?name=orig');
}
// Some grid thumbnails encode the size in the path (e.g. ?format=jpg with no name).
return url.includes('?') ? `${url}&name=orig` : `${url}?name=orig`;
};
const isDecorative = (src) => !src || src.includes('profile_images') || src.includes('emoji') || src.includes('profile_banners');
const getPostContext = (article) => {
const timeEl = article.querySelector('time');
const permalink = timeEl ? timeEl.closest('a[href*="/status/"]') : null;
const anchor = permalink || article.querySelector('a[href*="/status/"]');
const url = anchor ? anchor.href : '';
const id = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
const nameBlock = article.querySelector(SELECTORS.userName);
const handle = nameBlock ? (nameBlock.textContent.match(/@(\w+)/) || [, ''])[1] : '';
return { tweetId: id, tweetUrl: url, handle, timestamp: timeEl ? timeEl.getAttribute('datetime') : null };
};
const extractMedia = (article) => {
const out = [];
const ctx = getPostContext(article);
if (CONFIG.includeGifs) {
article.querySelectorAll(SELECTORS.gifThumb).forEach((gif) => {
if (!gif.src) return;
out.push({ type: 'gif', url: gif.src, ...ctx });
});
}
if (CONFIG.includeImages) {
article.querySelectorAll(SELECTORS.tweetPhoto).forEach((img) => {
if (isDecorative(img.src) || (img.src && img.src.includes('tweet_video_thumb'))) return;
const url = CONFIG.highestRes ? toOriginalImage(img.src) : img.src;
out.push({ type: 'image', url, ...ctx });
});
}
if (CONFIG.includeVideos) {
article.querySelectorAll(SELECTORS.video).forEach((v) => {
const url = v.src || v.currentSrc || v.poster || '';
if (!url) return;
out.push({ type: 'video', url, poster: v.poster || '', ...ctx });
});
}
return out;
};
const downloadFile = (content, filename, mime) => {
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
return true;
} catch (e) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const csvCell = (value) => `"${String(value == null ? '' : value).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const toCSV = (media) => {
const headers = ['type', 'url', 'handle', 'tweetId', 'tweetUrl', 'timestamp'];
const rows = media.map((m) => [
m.type, m.url, '@' + m.handle, m.tweetId, m.tweetUrl, m.timestamp || ''
].map(csvCell).join(','));
return [headers.join(','), ...rows].join('\n');
};
// ============================================
// 🎯 MAIN
// ============================================
const username = window.location.pathname.split('/').filter(Boolean)[0] || 'profile';
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🖼️ SCRAPE MEDIA - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn and return, never hard-redirect.
if (!/\/media\/?$/.test(window.location.pathname)) {
log.warning('You are not on a profile Media tab.');
log.info(`Go to x.com/${username}/media, then run this script again.`);
return;
}
const media = new Map();
// Stop switch: run window.stopScrapeMedia() to end the scroll loop.
let stopped = false;
window.stopScrapeMedia = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
log.info(`Profile: @${username}`);
log.info(`Max items: ${CONFIG.maxItems} | Highest resolution: ${CONFIG.highestRes}`);
log.info('To stop early: window.stopScrapeMedia()');
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && media.size < CONFIG.maxItems && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = media.size;
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
try {
extractMedia(article).forEach((item) => {
// Dedupe by the base URL (without query) so re-rendered thumbnails
// do not create duplicate entries.
const key = `${item.type}:${item.url.split('?')[0]}`;
if (!media.has(key)) media.set(key, item);
});
} catch (e) {
log.error(`Skipped a media item: ${e.message}`);
}
});
if (media.size > before) {
log.progress(media.size, CONFIG.maxItems);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewItemsThreshold) {
log.warning('No new media after several scrolls. Reached the end of the tab.');
break;
}
}
if (media.size >= CONFIG.maxItems) break;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
const list = Array.from(media.values()).slice(0, CONFIG.maxItems);
// ============================================
// 📊 SUMMARY + EXPORT
// ============================================
if (list.length === 0) {
log.warning('No media was found. The Media tab may be empty, or the grid had not loaded yet.');
return;
}
const images = list.filter((m) => m.type === 'image');
const videos = list.filter((m) => m.type === 'video');
const gifs = list.filter((m) => m.type === 'gif');
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE MEDIA - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 👤 Profile: ${String('@' + username).slice(0, 36).padEnd(37)}║
║ 🖼️ Total: ${String(list.length).padEnd(37)}║
║ 📷 Images: ${String(images.length).padEnd(37)}║
║ 🎥 Videos: ${String(videos.length).padEnd(37)}║
║ 🎞️ GIFs: ${String(gifs.length).padEnd(37)}║
╚══════════════════════════════════════════════════════════╝
`);
const stamp = new Date().toISOString().split('T')[0];
const result = {
username,
scrapedAt: new Date().toISOString(),
stats: { images: images.length, videos: videos.length, gifs: gifs.length, total: list.length },
media: list
};
if (downloadFile(JSON.stringify(result, null, 2), `${username}_media_${stamp}.json`, 'application/json')) {
log.success('JSON downloaded');
}
if (CONFIG.downloadUrlList) {
const urlText = list.map((m) => m.url).join('\n');
if (downloadFile(urlText, `${username}_media_urls_${stamp}.txt`, 'text/plain')) log.success('URL list (.txt) downloaded');
}
if (CONFIG.downloadCsv && downloadFile(toCSV(list), `${username}_media_${stamp}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
window.scrapedMedia = result;
window.exportCSV = () => toCSV(list);
console.log('');
log.info('window.scrapedMedia holds the full data. window.exportCSV() returns the CSV string.');
log.info('Open any URL from the JSON to save the file, or feed the .txt list to a downloader.');
log.success('Done! by nichxbt');
return result;
})();
});
register("scrape-notifications", function(){
(async function scrapeNotifications() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of notifications to collect
maxNotifications: 500,
// Maximum scroll attempts before giving up
maxScrollAttempts: 200,
// Stop after this many scrolls with no new notifications (end of feed)
noNewThreshold: 5,
// Delay between scrolls (ms). Raise if items load slowly.
scrollDelay: 1500,
// Auto-download the results as files when finished
downloadJSON: true,
downloadCSV: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
// Notification rows render as cellInnerDiv; older layouts use article
row: '[data-testid="cellInnerDiv"], article',
// Any profile link inside a row (used to extract actor handles)
profileLink: 'a[href^="/"]',
// Link to the related tweet, when the notification references one
statusLink: 'a[href*="/status/"]',
time: 'time'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
// Reserved handles that appear as links but are not actors
const NON_ACTOR = new Set([
'home', 'explore', 'notifications', 'messages', 'search', 'settings',
'i', 'compose', 'hashtag', 'intent'
]);
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const classifyType = (text) => {
const t = text.toLowerCase();
if (t.includes('liked')) return 'like';
if (t.includes('reposted') || t.includes('retweeted')) return 'retweet';
if (t.includes('followed')) return 'follow';
if (t.includes('quoted')) return 'quote';
if (t.includes('replied') || t.includes('replying to')) return 'reply';
if (t.includes('mentioned')) return 'mention';
if (t.includes('there was a login') || t.includes('new login')) return 'security';
if (t.includes('live') && t.includes('space')) return 'space';
return 'other';
};
const extractActors = (row) => {
const handles = [];
row.querySelectorAll(SELECTORS.profileLink).forEach(link => {
const href = link.getAttribute('href') || '';
// A bare "/handle" link, no deeper path, is a profile
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
if (!m) return;
const handle = m[1];
if (NON_ACTOR.has(handle.toLowerCase())) return;
if (!handles.includes(handle)) handles.push(handle);
});
return handles;
};
const extractNotification = (row) => {
const text = (row.textContent || '').trim();
if (text.length < 4) return null;
const actors = extractActors(row);
const statusLink = row.querySelector(SELECTORS.statusLink);
const tweetUrl = statusLink ? statusLink.href : '';
const timeEl = row.querySelector(SELECTORS.time);
const timestamp = timeEl ? (timeEl.getAttribute('datetime') || '') : '';
const displayTime = timeEl ? timeEl.textContent.trim() : '';
return {
type: classifyType(text),
actors,
text: text.slice(0, 500),
tweetUrl,
timestamp,
displayTime,
scrapedAt: new Date().toISOString()
};
};
// ============================================
// 🎯 MAIN
// ============================================
let stopped = false;
window.stopScrapeNotifications = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔔 SCRAPE NOTIFICATIONS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn (do not hard-redirect) if we are not on notifications
if (!window.location.pathname.includes('/notifications')) {
log.warning('You are not on the notifications page.');
log.info('Go to https://x.com/notifications, then paste this script again.');
return;
}
log.info(`Max notifications: ${CONFIG.maxNotifications}`);
log.info('To stop early: window.stopScrapeNotifications()');
console.log('');
const notifications = new Map();
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && notifications.size < CONFIG.maxNotifications && scrollAttempts < CONFIG.maxScrollAttempts) {
const rows = document.querySelectorAll(SELECTORS.row);
const before = notifications.size;
rows.forEach(row => {
try {
const n = extractNotification(row);
if (!n) return;
// Dedupe on type + actors + a text fingerprint
const key = `${n.type}|${n.actors.join(',')}|${n.text.slice(0, 80)}`;
if (!notifications.has(key)) notifications.set(key, n);
} catch (e) {
// One malformed row must not kill the run
}
});
const added = notifications.size - before;
if (added > 0) {
log.info(`Collected ${notifications.size} notifications (+${added})`);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewThreshold) {
log.warning('No new notifications after several scrolls. Reached the end.');
break;
}
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
const list = Array.from(notifications.values());
// ============================================
// 📊 SUMMARY
// ============================================
const byType = {};
const byActor = {};
list.forEach(n => {
byType[n.type] = (byType[n.type] || 0) + 1;
n.actors.forEach(a => { byActor[a] = (byActor[a] || 0) + 1; });
});
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ 📊 NOTIFICATIONS - COMPLETE ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log(`🔔 Total notifications: ${list.length}`);
console.log(`📜 Scroll attempts: ${scrollAttempts}`);
if (list.length === 0) {
log.warning('No notifications found. Your feed may be empty, or the page had not loaded.');
return { notifications: [] };
}
console.log('\n📊 By type:');
Object.entries(byType).sort((a, b) => b[1] - a[1]).forEach(([type, count]) => {
console.log(` ${type}: ${count}`);
});
const topActors = Object.entries(byActor).sort((a, b) => b[1] - a[1]).slice(0, 5);
if (topActors.length > 0) {
console.log('\n👥 Top interactors:');
topActors.forEach(([user, count]) => console.log(` @${user}: ${count}`));
}
// ============================================
// 💾 EXPORT
// ============================================
const dateStr = new Date().toISOString().split('T')[0];
const result = {
exportedAt: new Date().toISOString(),
count: list.length,
byType,
notifications: list
};
console.log('');
if (CONFIG.downloadJSON) {
if (download(JSON.stringify(result, null, 2), `notifications_${dateStr}.json`, 'application/json')) {
log.success('JSON downloaded');
}
}
if (CONFIG.downloadCSV) {
const esc = (v) => `"${String(v).replace(/"/g, '""').replace(/\n/g, ' ')}"`;
const csv = [
['Type', 'Actors', 'Text', 'TweetURL', 'Timestamp', 'DisplayTime'].join(','),
...list.map(n => [
esc(n.type),
esc(n.actors.join(' ')),
esc(n.text),
esc(n.tweetUrl),
esc(n.timestamp),
esc(n.displayTime)
].join(','))
].join('\n');
if (download(csv, `notifications_${dateStr}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
}
window.scrapedNotifications = result;
console.log('\n💡 Access your data: window.scrapedNotifications');
console.log('✅ Script completed! by nichxbt');
return result;
})();
});
register("scrape-profile-posts", function(){
var CONFIG = {
// ==========================================
// 📊 SCRAPING SETTINGS
// ==========================================
// Number of tweets to scrape (max)
// 💡 Set to a higher number for more tweets, but it takes longer
targetCount: 500,
// Maximum scroll attempts before giving up
// 💡 Increase if the profile has lots of media (slower loading)
maxScrollAttempts: 500,
// 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: 10,
// 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: true,
// 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
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 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) * 1000;
if (str.includes('M')) return parseFloat(str) * 1000000;
if (str.includes('B')) return parseFloat(str) * 1000000000;
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 the timestamp element first: its enclosing anchor is the tweet's
// own permalink. Grabbing the first /status/ link in the article can
// return a quoted tweet's URL and attribute the wrong ID.
const timeElement = tweet.querySelector('time');
const timestamp = timeElement ? timeElement.getAttribute('datetime') : null;
const displayTime = timeElement ? timeElement.innerText : '';
const permalinkAnchor = timeElement ? timeElement.closest('a[href*="/status/"]') : null;
const tweetLink = permalinkAnchor || 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 : '';
// 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"], [data-testid="videoComponent"]') !== null;
const hasCard = tweet.querySelector('[data-testid="card.wrapper"]') !== null;
// Retweets render socialContext as an <a> linking to the reposter's
// profile; pinned posts render it as a plain element. Checking the tag
// instead of the text keeps this working on non-English UIs and stops
// pinned posts being counted as retweets.
const socialContext = tweet.querySelector('[data-testid="socialContext"]');
const isRetweet = !!socialContext && socialContext.closest('a') !== null;
const isPinned = !!socialContext && !isRetweet;
// Reply detection: the "Replying to @user" line only renders with an
// English UI, so also check the structural marker X uses for replies.
const isReply = tweet.querySelector('[data-testid="in-reply-to"]') !== null ||
Array.from(tweet.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
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,
isPinned: isPinned
},
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();
// Track stalls independently of logging so the end-of-timeline check
// still works when verbose is off
if (tweets.length !== lastTweetCount) {
if (CONFIG.verbose) {
console.log(`📊 Progress: ${tweets.length}/${CONFIG.targetCount} tweets (${newCount} new this scroll)`);
}
lastTweetCount = tweets.length;
noNewTweetsCount = 0;
} else {
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) / 1000).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(60)}\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(60)}\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(60)}\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: 100%; 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, 300).replace(/</g, '<').replace(/>/g, '>')}${t.text.length > 300 ? '...' : ''}</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;
})();
});
register("scrape-profile-with-replies", function(){
var CONFIG = {
// ==========================================
// 📊 SCRAPING SETTINGS
// ==========================================
// Maximum number of the user's posts to collect from the profile feed.
// Set to Infinity to scrape everything until the feed ends.
targetPostCount: 50,
// Maximum replies to collect PER POST when drilling into a thread.
// Set higher for thorough sentiment analysis; lower for speed.
maxRepliesPerPost: 50,
// Maximum scroll attempts on the profile feed before giving up.
maxFeedScrollAttempts: 200,
// Maximum scroll attempts inside a single post's thread.
maxThreadScrollAttempts: 30,
// Delay between scrolls (ms). Increase if content doesn't load.
scrollDelay: 2000,
// Delay after navigating into/out of a post (ms). Lets the page settle.
navigationDelay: 3000,
// ==========================================
// 🎯 POST RANGE FILTER
// ==========================================
// Limit scraping to a range of posts by ID or URL.
// Leave null to scrape from the top of the feed.
range: {
// Start collecting posts AFTER this post (exclusive).
// Accepts a post ID (e.g. '1234567890') or full URL.
// null = start from the very first post in the feed.
startPostId: null,
// Stop collecting posts AFTER this post (inclusive).
// Accepts a post ID or full URL.
// null = scrape until targetPostCount or feed end.
endPostId: null,
},
// ==========================================
// 🔍 CONTENT FILTERS
// ==========================================
filters: {
// Only include posts containing at least one of these words.
// Empty array = include all.
whitelist: [],
// Exclude posts containing any of these words.
blacklist: [],
// Only posts from the last N days. 0 = no date limit.
daysBack: 0,
// Minimum engagement thresholds. 0 = no minimum.
minLikes: 0,
minRetweets: 0,
// true = skip retweets from the feed
excludeRetweets: false,
},
// ==========================================
// 📤 EXPORT SETTINGS
// ==========================================
export: {
json: true,
csv: true,
markdown: false,
text: false,
html: false,
},
// ==========================================
// 🖥️ CONTROL PANEL
// ==========================================
panel: {
// Show the floating control panel
enabled: true,
// Panel start position from top-right corner
top: 20,
right: 20,
},
// ==========================================
// 🔧 GENERAL
// ==========================================
// Copy primary export (JSON) to clipboard on completion
copyToClipboard: true,
// Show verbose progress in console
verbose: true,
// Also scrape replies for the user's own reply-tweets
// (when the user replied to someone else's post).
// If false, only scrapes replies on the user's original posts.
scrapeRepliesOnUserReplies: true,
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START — DO NOT MODIFY BELOW UNLESS YOU KNOW
* WHAT YOU'RE DOING
* ============================================================
*/
(async function XProfileRepliesScraper() {
'use strict';
// ─── State ──────────────────────────────────────────────
const state = {
phase: 'init', // 'init' | 'feed' | 'replies' | 'done'
paused: false,
stopped: false,
posts: [], // collected profile posts
currentPostIndex: -1, // index of post currently being reply-scraped
totalReplies: 0,
startTime: Date.now(),
feedScrolls: 0,
reachedStart: !CONFIG.range.startPostId, // if no start filter, we're already past it
reachedEnd: false,
originalUrl: window.location.href,
errors: [],
};
const seenPostIds = new Set();
// ─── Helpers ────────────────────────────────────────────
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Navigate within the SPA via history.pushState + a synthetic popstate.
// Assigning window.location.href triggers a full page reload, which wipes
// this console script's entire state (posts collected so far, panel, etc.)
// mid-run. Only fall back to a real navigation if the SPA route never
// takes (e.g. React hasn't mounted its popstate listener yet).
function spaNavigate(url) {
try {
const target = new URL(url, window.location.href);
if (target.origin === window.location.origin) {
window.history.pushState({}, '', target.href);
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
return true;
}
} catch (e) { /* fall through to hard navigation */ }
window.location.href = url;
return false;
}
/** Wait while paused, checking every 200ms. */
async function waitWhilePaused() {
while (state.paused && !state.stopped) {
await sleep(200);
}
}
/** Extract post ID from a URL or raw ID string. */
function normalizePostId(input) {
if (!input) return null;
input = String(input).trim();
// Full URL: https://x.com/user/status/1234567890
const match = input.match(/\/status\/(\d+)/);
if (match) return match[1];
// Raw numeric ID
if (/^\d+$/.test(input)) return input;
return null;
}
/** Parse engagement strings like '1.2K', '3M'. */
function parseEngagement(str) {
if (!str || str === '') return 0;
str = str.trim().toUpperCase();
if (str.includes('K')) return Math.round(parseFloat(str) * 1_000);
if (str.includes('M')) return Math.round(parseFloat(str) * 1_000_000);
if (str.includes('B')) return Math.round(parseFloat(str) * 1_000_000_000);
return parseInt(str.replace(/,/g, ''), 10) || 0;
}
function extractHashtags(text) {
return (text.match(/#[\w]+/g) || []);
}
function extractMentions(text) {
return (text.match(/@[\w]+/g) || []);
}
function extractUrls(text) {
return (text.match(/https?:\/\/[^\s]+/g) || []);
}
function elapsed() {
return ((Date.now() - state.startTime) / 1000).toFixed(1);
}
function log(msg) {
if (CONFIG.verbose) console.log(msg);
}
// Normalize range IDs
const startId = normalizePostId(CONFIG.range.startPostId);
const endId = normalizePostId(CONFIG.range.endPostId);
// ─── DOM extraction helpers ─────────────────────────────
/**
* Extract data from a single tweet article element.
* Returns null if it can't be parsed.
*/
function extractTweetFromElement(article) {
try {
// The timestamp's enclosing anchor is the tweet's own permalink.
// The first /status/ link in the article can belong to a quoted tweet.
const timeEl = article.querySelector('time');
const timestamp = timeEl ? timeEl.getAttribute('datetime') : null;
const displayTime = timeEl ? timeEl.innerText : '';
const permalinkEl = timeEl ? timeEl.closest('a[href*="/status/"]') : null;
const linkEl = permalinkEl || article.querySelector('a[href*="/status/"]');
if (!linkEl) return null;
const url = linkEl.href;
const id = url.split('/status/')[1]?.split(/[?/]/)[0];
if (!id) return null;
const textEl = article.querySelector('[data-testid="tweetText"]');
const text = textEl ? textEl.innerText : '';
const metric = (testId) => {
const el = article.querySelector(`[data-testid="${testId}"]`);
const span = el?.querySelector('span span');
return span ? span.innerText : '0';
};
const replies = metric('reply');
const retweets = metric('retweet');
const likes = metric('like');
const viewsEl = article.querySelector('a[href*="/analytics"]');
const views = viewsEl ? viewsEl.innerText : '0';
const hasImage = !!article.querySelector('[data-testid="tweetPhoto"]');
const hasVideo = !!article.querySelector('[data-testid="videoPlayer"], [data-testid="videoComponent"]');
const hasCard = !!article.querySelector('[data-testid="card.wrapper"]');
// Retweets render socialContext inside an <a> linking to the reposter;
// pinned posts render it as a plain element. The structural check works
// on any UI language and stops pinned posts counting as retweets.
const socialCtxEl = article.querySelector('[data-testid="socialContext"]');
const isRetweet = !!socialCtxEl && socialCtxEl.closest('a') !== null;
// Reply detection: structural marker first, English UI text as fallback.
const isReply = article.querySelector('[data-testid="in-reply-to"]') !== null ||
Array.from(article.querySelectorAll('div[dir]')).some(el =>
el.innerText.startsWith('Replying to'));
// Get the author handle from the tweet
const authorEl = article.querySelector('div[data-testid="User-Name"] a[href^="/"]');
const authorHandle = authorEl ? authorEl.getAttribute('href').replace('/', '') : '';
const authorNameEl = article.querySelector('div[data-testid="User-Name"] span');
const authorName = authorNameEl ? authorNameEl.innerText : '';
return {
id,
url,
text,
author: {
handle: authorHandle,
name: authorName,
},
timestamp,
displayTime,
metrics: { replies, retweets, likes, views },
media: { hasImage, hasVideo, hasCard },
type: { isRetweet, isReply },
extracted: {
hashtags: extractHashtags(text),
mentions: extractMentions(text),
urls: extractUrls(text),
},
scrapedAt: new Date().toISOString(),
};
} catch (e) {
return null;
}
}
/**
* Check whether a post passes all content filters.
*/
function passesFilters(tweet) {
const text = tweet.text.toLowerCase();
if (CONFIG.filters.whitelist.length > 0) {
if (!CONFIG.filters.whitelist.some(w => text.includes(w.toLowerCase()))) return false;
}
if (CONFIG.filters.blacklist.length > 0) {
if (CONFIG.filters.blacklist.some(w => text.includes(w.toLowerCase()))) return false;
}
if (CONFIG.filters.daysBack > 0 && tweet.timestamp) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - CONFIG.filters.daysBack);
if (new Date(tweet.timestamp) < cutoff) return false;
}
if (parseEngagement(tweet.metrics.likes) < CONFIG.filters.minLikes) return false;
if (parseEngagement(tweet.metrics.retweets) < CONFIG.filters.minRetweets) return false;
if (CONFIG.filters.excludeRetweets && tweet.type.isRetweet) return false;
return true;
}
// ─── Control Panel UI ──────────────────────────────────
let panelEl = null;
let statusEl = null;
let postsCountEl = null;
let repliesCountEl = null;
let phaseEl = null;
let elapsedEl = null;
let progressBarEl = null;
let currentPostEl = null;
let logAreaEl = null;
function createPanel() {
if (!CONFIG.panel.enabled) return;
// Remove existing panel if re-running
const existing = document.getElementById('x-scraper-panel');
if (existing) existing.remove();
panelEl = document.createElement('div');
panelEl.id = 'x-scraper-panel';
panelEl.innerHTML = `
<style>
#x-scraper-panel {
position: fixed;
top: ${CONFIG.panel.top}px;
right: ${CONFIG.panel.right}px;
width: 360px;
max-height: 90vh;
background: #15202b;
color: #e7e9ea;
border: 1px solid #38444d;
border-radius: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
z-index: 999999;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
overflow: hidden;
user-select: none;
}
#x-scraper-panel * { box-sizing: border-box; }
.xsp-header {
background: #1d9bf0;
padding: 12px 16px;
cursor: move;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 700;
font-size: 14px;
}
.xsp-header span { display: flex; align-items: center; gap: 6px; }
.xsp-body { padding: 12px 16px; }
.xsp-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid #38444d22;
}
.xsp-row .label { color: #8899a6; }
.xsp-row .value { font-weight: 600; font-variant-numeric: tabular-nums; }
.xsp-progress {
margin: 10px 0;
height: 6px;
background: #38444d;
border-radius: 3px;
overflow: hidden;
}
.xsp-progress-bar {
height: 100%;
background: #1d9bf0;
border-radius: 3px;
transition: width 0.3s ease;
width: 0%;
}
.xsp-current {
padding: 6px 8px;
margin: 8px 0;
background: #192734;
border-radius: 8px;
font-size: 12px;
color: #8899a6;
max-height: 40px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.xsp-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 10px;
}
.xsp-btn {
padding: 8px 12px;
border: none;
border-radius: 9999px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: opacity 0.2s;
text-align: center;
}
.xsp-btn:hover { opacity: 0.85; }
.xsp-btn:active { transform: scale(0.97); }
.xsp-btn-pause { background: #ffd166; color: #15202b; }
.xsp-btn-resume { background: #06d6a0; color: #15202b; }
.xsp-btn-stop { background: #ef476f; color: #fff; }
.xsp-btn-export { background: #1d9bf0; color: #fff; }
.xsp-btn-download { background: #8338ec; color: #fff; }
.xsp-btn-full { grid-column: 1 / -1; }
.xsp-log {
margin-top: 10px;
max-height: 100px;
overflow-y: auto;
font-size: 11px;
color: #8899a6;
background: #192734;
border-radius: 8px;
padding: 6px 8px;
}
.xsp-log div { padding: 1px 0; }
.xsp-log .warn { color: #ffd166; }
.xsp-log .err { color: #ef476f; }
.xsp-log .ok { color: #06d6a0; }
.xsp-minimize {
background: none;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
</style>
<div class="xsp-header" id="xsp-drag-handle">
<span>🐦 X Scraper</span>
<div style="display:flex;gap:4px;">
<button class="xsp-minimize" id="xsp-minimize" title="Minimize">−</button>
<button class="xsp-minimize" id="xsp-close" title="Close panel">×</button>
</div>
</div>
<div class="xsp-body" id="xsp-body">
<div class="xsp-row">
<span class="label">Phase</span>
<span class="value" id="xsp-phase">Initializing…</span>
</div>
<div class="xsp-row">
<span class="label">Status</span>
<span class="value" id="xsp-status">Running</span>
</div>
<div class="xsp-row">
<span class="label">Posts collected</span>
<span class="value" id="xsp-posts">0 / ${CONFIG.targetPostCount === Infinity ? '∞' : CONFIG.targetPostCount}</span>
</div>
<div class="xsp-row">
<span class="label">Replies collected</span>
<span class="value" id="xsp-replies">0</span>
</div>
<div class="xsp-row">
<span class="label">Elapsed</span>
<span class="value" id="xsp-elapsed">0s</span>
</div>
<div class="xsp-progress">
<div class="xsp-progress-bar" id="xsp-progress-bar"></div>
</div>
<div class="xsp-current" id="xsp-current">Preparing…</div>
<div class="xsp-buttons">
<button class="xsp-btn xsp-btn-pause" id="xsp-pause">⏸ Pause</button>
<button class="xsp-btn xsp-btn-stop" id="xsp-stop">⏹ Stop</button>
<button class="xsp-btn xsp-btn-export" id="xsp-export">📦 Export Now</button>
<button class="xsp-btn xsp-btn-download" id="xsp-download">💾 Download</button>
<button class="xsp-btn xsp-btn-export xsp-btn-full" id="xsp-pause-export" style="background:#118ab2;">⏸ Pause & Export</button>
</div>
<div class="xsp-log" id="xsp-log"></div>
</div>
`;
document.body.appendChild(panelEl);
// Cache elements
statusEl = document.getElementById('xsp-status');
postsCountEl = document.getElementById('xsp-posts');
repliesCountEl = document.getElementById('xsp-replies');
phaseEl = document.getElementById('xsp-phase');
elapsedEl = document.getElementById('xsp-elapsed');
progressBarEl = document.getElementById('xsp-progress-bar');
currentPostEl = document.getElementById('xsp-current');
logAreaEl = document.getElementById('xsp-log');
// ── Dragging ──
let isDragging = false, dragX, dragY;
const handle = document.getElementById('xsp-drag-handle');
handle.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
dragX = e.clientX - panelEl.offsetLeft;
dragY = e.clientY - panelEl.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panelEl.style.left = (e.clientX - dragX) + 'px';
panelEl.style.right = 'auto';
panelEl.style.top = (e.clientY - dragY) + 'px';
});
document.addEventListener('mouseup', () => { isDragging = false; });
// ── Minimize ──
const body = document.getElementById('xsp-body');
document.getElementById('xsp-minimize').addEventListener('click', () => {
body.style.display = body.style.display === 'none' ? 'block' : 'none';
});
// ── Close ──
document.getElementById('xsp-close').addEventListener('click', () => {
panelEl.style.display = panelEl.style.display === 'none' ? 'block' : 'none';
});
// ── Buttons ──
document.getElementById('xsp-pause').addEventListener('click', () => {
if (state.paused) {
state.paused = false;
updateStatus('Running');
panelLog('▶ Resumed', 'ok');
document.getElementById('xsp-pause').textContent = '⏸ Pause';
document.getElementById('xsp-pause').className = 'xsp-btn xsp-btn-pause';
} else {
state.paused = true;
updateStatus('Paused');
panelLog('⏸ Paused', 'warn');
document.getElementById('xsp-pause').textContent = '▶ Resume';
document.getElementById('xsp-pause').className = 'xsp-btn xsp-btn-resume';
}
});
document.getElementById('xsp-stop').addEventListener('click', () => {
state.stopped = true;
state.paused = false;
updateStatus('Stopped');
panelLog('⏹ Stopped by user', 'warn');
});
document.getElementById('xsp-export').addEventListener('click', () => {
panelLog('📦 Exporting…', 'ok');
exportAllFormats();
});
document.getElementById('xsp-download').addEventListener('click', () => {
panelLog('💾 Downloading…', 'ok');
downloadAllFormats();
});
document.getElementById('xsp-pause-export').addEventListener('click', () => {
state.paused = true;
updateStatus('Paused');
document.getElementById('xsp-pause').textContent = '▶ Resume';
document.getElementById('xsp-pause').className = 'xsp-btn xsp-btn-resume';
panelLog('⏸ Paused & exporting…', 'warn');
exportAllFormats();
});
// Timer
setInterval(() => {
if (elapsedEl) elapsedEl.textContent = elapsed() + 's';
}, 1000);
}
function updateStatus(s) {
if (statusEl) statusEl.textContent = s;
}
function updatePhase(p) {
if (phaseEl) phaseEl.textContent = p;
state.phase = p;
}
function updatePostsCount() {
const target = CONFIG.targetPostCount === Infinity ? '∞' : CONFIG.targetPostCount;
if (postsCountEl) postsCountEl.textContent = `${state.posts.length} / ${target}`;
}
function updateRepliesCount() {
if (repliesCountEl) repliesCountEl.textContent = String(state.totalReplies);
}
function updateProgress(pct) {
if (progressBarEl) progressBarEl.style.width = Math.min(100, pct).toFixed(1) + '%';
}
function updateCurrent(msg) {
if (currentPostEl) currentPostEl.textContent = msg;
}
function panelLog(msg, cls = '') {
log(msg);
if (!logAreaEl) return;
const d = document.createElement('div');
if (cls) d.className = cls;
d.textContent = `[${elapsed()}s] ${msg}`;
logAreaEl.prepend(d);
// Keep log under 200 entries
while (logAreaEl.children.length > 200) logAreaEl.lastChild.remove();
}
// ─── Export helpers ─────────────────────────────────────
function buildResult() {
const profileMatch = state.originalUrl.match(/x\.com\/([^\/]+)/);
const profileName = profileMatch ? profileMatch[1] : 'unknown';
const totalReplyCount = state.posts.reduce((sum, p) => sum + (p.replies_scraped?.length || 0), 0);
return {
profile: profileName,
profileUrl: `https://x.com/${profileName}`,
scrapedAt: new Date().toISOString(),
duration: elapsed() + 's',
totalPosts: state.posts.length,
totalRepliesScraped: totalReplyCount,
config: {
targetPostCount: CONFIG.targetPostCount,
maxRepliesPerPost: CONFIG.maxRepliesPerPost,
startPostId: startId,
endPostId: endId,
filters: CONFIG.filters,
},
posts: state.posts,
};
}
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('Download failed:', e);
return false;
}
}
function toCSV(result) {
// Posts CSV
const postHeaders = ['PostID', 'URL', 'Author', 'Date', 'Text', 'Likes', 'Retweets', 'Replies', 'Views', 'IsRetweet', 'IsReply', 'Hashtags', 'RepliesScraped'];
const postRows = result.posts.map(p => [
p.id,
p.url,
p.author?.handle || '',
'"' + (p.displayTime || '').replace(/"/g, '""') + '"',
'"' + (p.text || '').replace(/"/g, '""').replace(/\n/g, ' ') + '"',
parseEngagement(p.metrics.likes),
parseEngagement(p.metrics.retweets),
parseEngagement(p.metrics.replies),
parseEngagement(p.metrics.views),
p.type.isRetweet,
p.type.isReply,
'"' + (p.extracted?.hashtags || []).join(' ') + '"',
(p.replies_scraped || []).length,
].join(','));
// Replies CSV
const replyHeaders = ['ParentPostID', 'ReplyID', 'URL', 'Author', 'AuthorHandle', 'Date', 'Text', 'Likes', 'Retweets', 'Replies', 'Views'];
const replyRows = [];
result.posts.forEach(p => {
(p.replies_scraped || []).forEach(r => {
replyRows.push([
p.id,
r.id,
r.url,
'"' + (r.author?.name || '').replace(/"/g, '""') + '"',
r.author?.handle || '',
'"' + (r.displayTime || '').replace(/"/g, '""') + '"',
'"' + (r.text || '').replace(/"/g, '""').replace(/\n/g, ' ') + '"',
parseEngagement(r.metrics?.likes),
parseEngagement(r.metrics?.retweets),
parseEngagement(r.metrics?.replies),
parseEngagement(r.metrics?.views),
].join(','));
});
});
const postsCSV = [postHeaders.join(','), ...postRows].join('\n');
const repliesCSV = [replyHeaders.join(','), ...replyRows].join('\n');
return { postsCSV, repliesCSV };
}
function toMarkdown(result) {
let md = `# X Posts & Replies — @${result.profile}\n\n`;
md += `> Scraped: ${result.scrapedAt} \n`;
md += `> Posts: ${result.totalPosts} | Replies: ${result.totalRepliesScraped} \n`;
md += `> Duration: ${result.duration}\n\n`;
result.posts.forEach((p, i) => {
md += `---\n\n`;
md += `## Post ${i + 1} — ${p.displayTime}\n\n`;
md += `**@${p.author?.handle || '?'}**: ${p.text}\n\n`;
md += `❤️ ${p.metrics.likes} | 🔄 ${p.metrics.retweets} | 💬 ${p.metrics.replies} | 👁️ ${p.metrics.views} \n`;
md += `[View post](${p.url})\n\n`;
if (p.replies_scraped?.length > 0) {
md += `### Replies (${p.replies_scraped.length})\n\n`;
p.replies_scraped.forEach((r, j) => {
md += `> **${j + 1}. @${r.author?.handle || '?'}** (${r.displayTime}): ${r.text} \n`;
md += `> ❤️ ${r.metrics?.likes || 0} | 💬 ${r.metrics?.replies || 0} \n\n`;
});
}
});
return md;
}
function toPlainText(result) {
let txt = `X POSTS & REPLIES — @${result.profile}\n`;
txt += `${'='.repeat(60)}\n`;
txt += `Scraped: ${result.scrapedAt}\n`;
txt += `Posts: ${result.totalPosts} | Replies: ${result.totalRepliesScraped}\n\n`;
result.posts.forEach((p, i) => {
txt += `${'─'.repeat(60)}\n`;
txt += `POST ${i + 1} — @${p.author?.handle || '?'} — ${p.displayTime}\n`;
txt += `${'─'.repeat(60)}\n`;
txt += `${p.text}\n\n`;
txt += `Likes: ${p.metrics.likes} | RTs: ${p.metrics.retweets} | Replies: ${p.metrics.replies} | Views: ${p.metrics.views}\n`;
txt += `URL: ${p.url}\n\n`;
if (p.replies_scraped?.length > 0) {
txt += ` REPLIES (${p.replies_scraped.length}):\n`;
p.replies_scraped.forEach((r, j) => {
txt += ` ${j + 1}. @${r.author?.handle || '?'} (${r.displayTime}): ${r.text}\n`;
txt += ` Likes: ${r.metrics?.likes || 0}\n\n`;
});
}
txt += '\n';
});
return txt;
}
function toHTML(result) {
const esc = s => String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
let html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>X Posts — @${esc(result.profile)}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; background: #f7f9fa; color: #0f1419; }
h1 { color: #1d9bf0; }
.meta { color: #536471; margin-bottom: 24px; }
.post { background: #fff; border: 1px solid #eff3f4; border-radius: 16px; padding: 16px; margin-bottom: 16px; }
.post-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
.handle { font-weight: 700; color: #1d9bf0; }
.post-text { margin: 8px 0; white-space: pre-wrap; }
.metrics { color: #536471; font-size: 13px; }
.replies { margin-top: 12px; padding-left: 16px; border-left: 3px solid #1d9bf0; }
.reply { padding: 8px 0; border-bottom: 1px solid #eff3f4; }
.reply:last-child { border-bottom: none; }
.reply-handle { font-weight: 600; color: #536471; }
a { color: #1d9bf0; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>🐦 X Posts & Replies — @${esc(result.profile)}</h1>
<div class="meta">
Scraped: ${esc(result.scrapedAt)} | Posts: ${result.totalPosts} | Replies: ${result.totalRepliesScraped} | Duration: ${esc(result.duration)}
</div>
`;
result.posts.forEach((p, i) => {
html += `<div class="post">
<div class="post-header">
<span class="handle">@${esc(p.author?.handle)}</span>
<span>${esc(p.displayTime)}</span>
</div>
<div class="post-text">${esc(p.text)}</div>
<div class="metrics">❤️ ${esc(p.metrics.likes)} | 🔄 ${esc(p.metrics.retweets)} | 💬 ${esc(p.metrics.replies)} | 👁️ ${esc(p.metrics.views)} — <a href="${esc(p.url)}" target="_blank">View</a></div>
`;
if (p.replies_scraped?.length > 0) {
html += ` <div class="replies"><strong>Replies (${p.replies_scraped.length})</strong>\n`;
p.replies_scraped.forEach(r => {
html += ` <div class="reply">
<span class="reply-handle">@${esc(r.author?.handle)}</span> <span style="color:#536471;font-size:12px;">${esc(r.displayTime)}</span><br>
${esc(r.text)}<br>
<span style="color:#536471;font-size:12px;">❤️ ${esc(r.metrics?.likes || '0')}</span>
</div>\n`;
});
html += ` </div>\n`;
}
html += `</div>\n`;
});
html += `</body></html>`;
return html;
}
function exportAllFormats() {
const result = buildResult();
const dateStr = new Date().toISOString().split('T')[0];
const profileName = result.profile;
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📦 EXPORTING DATA ║');
console.log('╚════════════════════════════════════════════════════════════╝');
if (CONFIG.export.json) {
const json = JSON.stringify(result, null, 2);
console.log(`📎 JSON: ${(json.length / 1024).toFixed(1)} KB`);
if (CONFIG.copyToClipboard) {
navigator.clipboard.writeText(json).then(
() => panelLog('📋 JSON copied to clipboard', 'ok'),
() => panelLog('⚠ Clipboard copy failed', 'warn')
);
}
}
if (CONFIG.export.csv) {
const { postsCSV, repliesCSV } = toCSV(result);
console.log(`📎 Posts CSV: ${(postsCSV.length / 1024).toFixed(1)} KB`);
console.log(`📎 Replies CSV: ${(repliesCSV.length / 1024).toFixed(1)} KB`);
}
console.log('✅ Export data ready! Use 💾 Download to save files.');
panelLog('📦 Export complete', 'ok');
// Store on window for console access
window.__xScraperResult = result;
console.log('💡 Tip: Access data via window.__xScraperResult');
return result;
}
function downloadAllFormats() {
const result = buildResult();
const dateStr = new Date().toISOString().split('T')[0];
const profileName = result.profile;
let count = 0;
if (CONFIG.export.json) {
downloadFile(JSON.stringify(result, null, 2), `${profileName}-posts-replies-${dateStr}.json`, 'application/json');
count++;
}
if (CONFIG.export.csv) {
const { postsCSV, repliesCSV } = toCSV(result);
downloadFile(postsCSV, `${profileName}-posts-${dateStr}.csv`, 'text/csv');
downloadFile(repliesCSV, `${profileName}-replies-${dateStr}.csv`, 'text/csv');
count += 2;
}
if (CONFIG.export.markdown) {
downloadFile(toMarkdown(result), `${profileName}-posts-replies-${dateStr}.md`, 'text/markdown');
count++;
}
if (CONFIG.export.text) {
downloadFile(toPlainText(result), `${profileName}-posts-replies-${dateStr}.txt`, 'text/plain');
count++;
}
if (CONFIG.export.html) {
downloadFile(toHTML(result), `${profileName}-posts-replies-${dateStr}.html`, 'text/html');
count++;
}
panelLog(`💾 Downloaded ${count} file(s)`, 'ok');
}
// ─── Phase 1: Scrape profile feed ──────────────────────
async function scrapeProfileFeed() {
updatePhase('📜 Scraping profile feed…');
panelLog('Phase 1: Collecting posts from feed');
let noNewCount = 0;
let prevCount = 0;
while (
state.posts.length < CONFIG.targetPostCount &&
state.feedScrolls < CONFIG.maxFeedScrollAttempts &&
!state.stopped &&
!state.reachedEnd
) {
await waitWhilePaused();
if (state.stopped) break;
// Extract visible tweets
const articles = document.querySelectorAll('article[data-testid="tweet"]');
let newThisRound = 0;
for (const article of articles) {
if (state.posts.length >= CONFIG.targetPostCount || state.reachedEnd) break;
const tweet = extractTweetFromElement(article);
if (!tweet || seenPostIds.has(tweet.id)) continue;
seenPostIds.add(tweet.id);
// ── Range: Start filter ──
if (!state.reachedStart) {
if (startId && tweet.id === startId) {
state.reachedStart = true;
panelLog(`🎯 Reached start post ${startId}`, 'ok');
}
continue; // skip posts before startId
}
// ── Range: End filter ──
if (endId && tweet.id === endId) {
state.reachedEnd = true;
panelLog(`🏁 Reached end post ${endId}`, 'ok');
}
// Apply content filters
if (!passesFilters(tweet)) continue;
// Initialize reply storage
tweet.replies_scraped = [];
state.posts.push(tweet);
newThisRound++;
updatePostsCount();
}
if (newThisRound > 0) {
noNewCount = 0;
const pct = CONFIG.targetPostCount === Infinity
? 50 // indeterminate
: (state.posts.length / CONFIG.targetPostCount) * 50;
updateProgress(pct);
updateCurrent(`Collected ${state.posts.length} posts…`);
} else {
noNewCount++;
if (noNewCount >= 8) {
panelLog('⚠ No new posts after 8 scrolls. Feed may have ended.', 'warn');
break;
}
}
prevCount = state.posts.length;
// Scroll
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
state.feedScrolls++;
}
panelLog(`✅ Feed done: ${state.posts.length} posts collected`, 'ok');
}
// ─── Phase 2: Scrape replies for each post ─────────────
/**
* Navigate to a tweet's page by simulating a click on the timestamp link,
* which triggers X's SPA client-side routing (no full page reload).
*/
async function navigateToTweet(postUrl) {
// Find the tweet article with this URL and click its timestamp link
const links = document.querySelectorAll('a[href*="/status/"]');
let targetLink = null;
for (const link of links) {
if (link.href === postUrl) {
// Prefer the time element link (more reliable for navigation)
const time = link.querySelector('time');
if (time) {
targetLink = link;
break;
}
}
}
if (targetLink) {
targetLink.click();
} else {
// Fallback: the post's own article scrolled out of view / was
// virtualized away, so there's no link to click. Use SPA navigation
// (not a hard window.location.href reload, which would kill this
// entire running scraper and lose all progress collected so far).
spaNavigate(postUrl);
}
await sleep(CONFIG.navigationDelay);
}
async function navigateBack() {
window.history.back();
await sleep(CONFIG.navigationDelay);
}
/**
* Once on a tweet detail page, extract replies from the thread.
* Replies appear as articles below the main tweet.
*/
async function scrapeThreadReplies(parentPostId) {
const replies = [];
const seenReplyIds = new Set();
let scrollAttempts = 0;
let noNewCount = 0;
// Wait for tweet thread to load
await sleep(1000);
while (
replies.length < CONFIG.maxRepliesPerPost &&
scrollAttempts < CONFIG.maxThreadScrollAttempts &&
!state.stopped
) {
await waitWhilePaused();
if (state.stopped) break;
const articles = document.querySelectorAll('article[data-testid="tweet"]');
let newThisRound = 0;
// The first article is usually the main tweet itself; replies follow.
// We identify replies as tweets whose ID differs from parentPostId.
for (const article of articles) {
const tweet = extractTweetFromElement(article);
if (!tweet) continue;
if (tweet.id === parentPostId) continue; // skip the main tweet
if (seenReplyIds.has(tweet.id)) continue;
seenReplyIds.add(tweet.id);
replies.push(tweet);
newThisRound++;
state.totalReplies++;
updateRepliesCount();
if (replies.length >= CONFIG.maxRepliesPerPost) break;
}
if (newThisRound > 0) {
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= 4) break; // no more replies loading
}
// Scroll to load more replies
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
return replies;
}
async function scrapeAllReplies() {
updatePhase('💬 Scraping replies…');
panelLog(`Phase 2: Scraping replies for ${state.posts.length} posts`);
for (let i = 0; i < state.posts.length; i++) {
if (state.stopped) break;
await waitWhilePaused();
const post = state.posts[i];
state.currentPostIndex = i;
// Skip user-reply tweets if configured
if (!CONFIG.scrapeRepliesOnUserReplies && post.type.isReply) {
panelLog(`⏭ Skipping reply-tweet ${i + 1}/${state.posts.length}`);
continue;
}
const replyCountNum = parseEngagement(post.metrics.replies);
if (replyCountNum === 0) {
panelLog(`⏭ Post ${i + 1}/${state.posts.length} — 0 replies, skipping`);
continue;
}
updateCurrent(`Post ${i + 1}/${state.posts.length}: scraping replies (${post.metrics.replies} reported)…`);
panelLog(`💬 Post ${i + 1}/${state.posts.length} — scraping replies…`);
const progressBase = 50;
const progressPerPost = 50 / state.posts.length;
updateProgress(progressBase + progressPerPost * i);
try {
// Navigate to the tweet
await navigateToTweet(post.url);
await sleep(1000);
// Check we landed on the right page
const onTweet = window.location.href.includes('/status/');
if (!onTweet) {
panelLog(`⚠ Navigation failed for post ${i + 1}, retrying…`, 'warn');
// SPA navigation, not window.location.href: a hard reload here
// would destroy this running script and lose every post/reply
// collected so far.
spaNavigate(post.url);
await sleep(CONFIG.navigationDelay + 1000);
}
// Scrape replies
const replies = await scrapeThreadReplies(post.id);
post.replies_scraped = replies;
panelLog(` → ${replies.length} replies collected`, 'ok');
// Navigate back to profile
await navigateBack();
// Wait for feed to re-render
await sleep(1000);
// We may need to scroll back to where we were. Wait for articles to load.
let attempts = 0;
while (attempts < 5) {
const hasArticles = document.querySelectorAll('article[data-testid="tweet"]').length > 0;
if (hasArticles) break;
await sleep(1000);
attempts++;
}
} catch (err) {
panelLog(`⚠ Error on post ${i + 1}: ${err.message}`, 'err');
state.errors.push({ postId: post.id, error: err.message });
// Try to get back to the profile. SPA navigation, not a hard
// reload: this runs mid-loop, so destroying the script here would
// silently abandon every remaining post.
try {
if (!window.location.href.includes(state.originalUrl.split('x.com/')[1]?.split('/')[0])) {
spaNavigate(state.originalUrl);
await sleep(CONFIG.navigationDelay);
}
} catch (_) {
// ignore
}
}
}
panelLog(`✅ Replies done: ${state.totalReplies} replies total`, 'ok');
}
// ─── Main ──────────────────────────────────────────────
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🐦 X Profile + Replies Scraper ║');
console.log('║ by nichxbt — https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on the right page
if (!window.location.href.includes('x.com/')) {
console.error('❌ This script must be run on x.com. Navigate to a profile page first.');
return;
}
const profileMatch = window.location.pathname.match(/^\/([^\/]+)/);
const profileName = profileMatch ? profileMatch[1] : 'unknown';
console.log(`🎯 Profile: @${profileName}`);
console.log(`📊 Target: ${CONFIG.targetPostCount === Infinity ? '∞' : CONFIG.targetPostCount} posts`);
console.log(`💬 Max replies per post: ${CONFIG.maxRepliesPerPost}`);
if (startId) console.log(`🏁 Start after post: ${startId}`);
if (endId) console.log(`🏁 Stop at post: ${endId}`);
console.log('');
// Ensure we're on with_replies
if (!window.location.href.includes('/with_replies')) {
console.log('💡 Tip: Navigate to /with_replies to include the user\'s replies too.');
console.log(` https://x.com/${profileName}/with_replies`);
console.log('');
}
// Create control panel
createPanel();
panelLog('🚀 Scraper initialized');
try {
// Phase 1: Collect posts from the profile feed
await scrapeProfileFeed();
if (state.stopped) {
panelLog('Stopped during feed scraping', 'warn');
}
if (state.posts.length === 0) {
panelLog('⚠ No posts found! Check that you\'re on the right profile page.', 'err');
updatePhase('⚠ No posts found');
updateStatus('Done (no posts)');
return;
}
// Phase 2: Scrape replies for each post
if (!state.stopped) {
// Scroll back to top before navigating into posts
window.scrollTo(0, 0);
await sleep(1000);
await scrapeAllReplies();
}
// Done
updatePhase('✅ Complete');
updateStatus('Done');
updateProgress(100);
const result = buildResult();
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ SCRAPING COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
console.log(`👤 Profile: @${result.profile}`);
console.log(`📊 Posts collected: ${result.totalPosts}`);
console.log(`💬 Replies collected: ${result.totalRepliesScraped}`);
console.log(`⏱️ Duration: ${result.duration}`);
console.log('');
// Stats
let totalLikes = 0, totalRetweets = 0;
result.posts.forEach(p => {
totalLikes += parseEngagement(p.metrics.likes);
totalRetweets += parseEngagement(p.metrics.retweets);
});
console.log('📈 ─── POST STATISTICS ───');
console.log(` Total Likes: ${totalLikes.toLocaleString()}`);
console.log(` Total Retweets: ${totalRetweets.toLocaleString()}`);
console.log(` Avg Likes/Post: ${result.totalPosts > 0 ? Math.round(totalLikes / result.totalPosts).toLocaleString() : 0}`);
console.log(` Avg Replies Scraped/Post: ${result.totalPosts > 0 ? (result.totalRepliesScraped / result.totalPosts).toFixed(1) : 0}`);
console.log('');
if (state.errors.length > 0) {
console.log(`⚠️ ${state.errors.length} error(s) occurred:`);
state.errors.forEach(e => console.log(` Post ${e.postId}: ${e.error}`));
console.log('');
}
// Top posts by reply count
const byReplies = [...result.posts].sort((a, b) =>
(b.replies_scraped?.length || 0) - (a.replies_scraped?.length || 0)
).slice(0, 5);
if (byReplies.length > 0) {
console.log('🏆 ─── TOP POSTS (by replies scraped) ───');
byReplies.forEach((p, i) => {
console.log(` ${i + 1}. [${p.replies_scraped?.length || 0} replies] ${p.text?.substring(0, 60)}…`);
console.log(` ${p.url}`);
});
console.log('');
}
// Auto-export
panelLog('Running auto-export…', 'ok');
downloadAllFormats();
// Store on window
window.__xScraperResult = result;
console.log('💡 Access full data: window.__xScraperResult');
console.log('💡 Re-download: Use the 💾 Download button on the panel');
} catch (fatalErr) {
console.error('❌ Fatal error:', fatalErr);
panelLog(`❌ Fatal: ${fatalErr.message}`, 'err');
updatePhase('❌ Error');
updateStatus('Error');
// Still try to export whatever we have
if (state.posts.length > 0) {
panelLog('Attempting partial export…', 'warn');
downloadAllFormats();
window.__xScraperResult = buildResult();
}
}
})();
});
register("scrape-replies", function(){
(async function scrapeReplies() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of replies to collect before stopping
maxReplies: 500,
// Delay between scrolls (ms). Raise if replies load slowly.
scrollDelay: 1800,
// Give up after this many scrolls that add no new replies
noNewRepliesThreshold: 6,
// Hard cap on scroll attempts so the loop can never run forever
maxScrollAttempts: 400,
// Auto-download a CSV file too (JSON always downloads)
downloadCsv: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userName: '[data-testid="User-Name"]',
actionGroup: '[role="group"]',
photo: '[data-testid="tweetPhoto"] img',
video: 'video'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (n, total) => console.log(`📊 Collected ${n}/${total} replies...`)
};
const countFromLabel = (label) => {
if (!label) return 0;
const raw = (label.match(/[\d.,]+\s*[KMB]?/i) || ['0'])[0].trim().toUpperCase();
const num = parseFloat(raw.replace(/,/g, '')) || 0;
if (raw.includes('K')) return Math.round(num * 1e3);
if (raw.includes('M')) return Math.round(num * 1e6);
if (raw.includes('B')) return Math.round(num * 1e9);
return Math.round(num);
};
const getCounts = (article) => {
const counts = { replies: 0, reposts: 0, likes: 0, views: 0 };
const group = article.querySelector(SELECTORS.actionGroup);
const nodes = group ? group.querySelectorAll('[aria-label]') : [];
nodes.forEach((node) => {
const label = (node.getAttribute('aria-label') || '').toLowerCase();
const value = countFromLabel(label);
if (label.includes('repl')) counts.replies = value;
else if (label.includes('repost') || label.includes('retweet')) counts.reposts = value;
else if (label.includes('like')) counts.likes = value;
else if (label.includes('view')) counts.views = value;
});
return counts;
};
const toOriginalImage = (url) => {
if (!url) return url;
return url.replace(/&name=[^&]*/i, '&name=orig').replace(/\?name=[^&]*/i, '?name=orig');
};
const getMediaUrls = (article) => {
const urls = new Set();
article.querySelectorAll(SELECTORS.photo).forEach((img) => {
if (img.src) urls.add(toOriginalImage(img.src));
});
article.querySelectorAll(SELECTORS.video).forEach((v) => {
if (v.src) urls.add(v.src);
else if (v.poster) urls.add(v.poster);
});
return Array.from(urls);
};
const extractReply = (article) => {
const timeEl = article.querySelector('time');
const permalink = timeEl ? timeEl.closest('a[href*="/status/"]') : null;
const anchor = permalink || article.querySelector('a[href*="/status/"]');
const url = anchor ? anchor.href : '';
const id = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
if (!id) return null;
const nameBlock = article.querySelector(SELECTORS.userName);
const nameText = nameBlock ? nameBlock.textContent : '';
const handle = (nameText.match(/@(\w+)/) || [, ''])[1];
const name = nameText.split('@')[0].trim();
const verified = !!article.querySelector(`${SELECTORS.userName} svg[aria-label*="Verified"]`);
const textEl = article.querySelector(SELECTORS.tweetText);
const text = textEl ? textEl.textContent : '';
const counts = getCounts(article);
return {
id,
url,
author: { handle, name },
verified,
text,
likes: counts.likes,
reposts: counts.reposts,
replies: counts.replies,
views: counts.views,
timestamp: timeEl ? timeEl.getAttribute('datetime') : null,
mediaUrls: getMediaUrls(article),
scrapedAt: new Date().toISOString()
};
};
const downloadFile = (content, filename, mime) => {
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
return true;
} catch (e) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const csvCell = (value) => `"${String(value == null ? '' : value).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const toCSV = (replies) => {
const headers = ['id', 'handle', 'name', 'verified', 'text', 'likes', 'reposts', 'replies', 'views', 'timestamp', 'url'];
const rows = replies.map((r) => [
r.id, '@' + r.author.handle, r.author.name, r.verified, r.text,
r.likes, r.reposts, r.replies, r.views, r.timestamp || '', r.url
].map(csvCell).join(','));
return [headers.join(','), ...rows].join('\n');
};
const getStatusId = () => {
const match = window.location.pathname.match(/status\/(\d+)/);
return match ? match[1] : null;
};
const getOriginalAuthor = () => {
const match = window.location.pathname.match(/^\/(\w+)\/status\//);
return match ? match[1] : null;
};
// ============================================
// 🎯 MAIN
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 💬 SCRAPE REPLIES - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
const statusId = getStatusId();
const originalAuthor = getOriginalAuthor();
// Page guard: warn and return, never hard-redirect.
if (!statusId) {
log.warning('You are not on a post status page.');
log.info('Open a post (x.com/<user>/status/<id>), then run this script again.');
return;
}
const replies = new Map();
// Stop switch: run window.stopScrapeReplies() to end the scroll loop.
let stopped = false;
window.stopScrapeReplies = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
log.info(`Original post: @${originalAuthor || 'unknown'} / ${statusId}`);
log.info(`Max replies: ${CONFIG.maxReplies}`);
log.info('To stop early: window.stopScrapeReplies()');
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && replies.size < CONFIG.maxReplies && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = replies.size;
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
try {
const reply = extractReply(article);
if (!reply) return;
// Skip the original post: it shares the status id in the URL.
if (reply.id === statusId) return;
if (!replies.has(reply.id)) replies.set(reply.id, reply);
} catch (e) {
log.error(`Skipped a reply: ${e.message}`);
}
});
if (replies.size > before) {
log.progress(replies.size, CONFIG.maxReplies);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewRepliesThreshold) {
log.warning('No new replies after several scrolls. Reached the end of the conversation.');
break;
}
}
if (replies.size >= CONFIG.maxReplies) break;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
const list = Array.from(replies.values()).slice(0, CONFIG.maxReplies);
// ============================================
// 📊 SUMMARY + EXPORT
// ============================================
if (list.length === 0) {
log.warning('No replies were found. The post may have no replies, or the thread had not loaded yet.');
return;
}
const uniqueUsers = new Set(list.map((r) => r.author.handle)).size;
const verifiedCount = list.filter((r) => r.verified).length;
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE REPLIES - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 💬 Replies: ${String(list.length).padEnd(37)}║
║ 👥 Unique users: ${String(uniqueUsers).padEnd(37)}║
║ ✓ Verified: ${String(verifiedCount).padEnd(37)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((r, i) => {
console.log(`${i + 1}. @${r.author.handle}${r.verified ? ' ✓' : ''} (${r.likes} ❤️): "${r.text.slice(0, 60)}"`);
});
const stamp = new Date().toISOString().split('T')[0];
const result = {
originalStatusId: statusId,
originalAuthor,
scrapedAt: new Date().toISOString(),
count: list.length,
replies: list
};
if (downloadFile(JSON.stringify(result, null, 2), `replies_${statusId}_${stamp}.json`, 'application/json')) {
log.success('JSON downloaded');
}
if (CONFIG.downloadCsv && downloadFile(toCSV(list), `replies_${statusId}_${stamp}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
window.scrapedReplies = result;
window.exportCSV = () => toCSV(list);
console.log('');
log.info('window.scrapedReplies holds the full data. window.exportCSV() returns the CSV string.');
log.success('Done! by nichxbt');
return result;
})();
});
register("scrape-retweeters", function(){
(async function scrapeReposters() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Collect users who reposted (the /retweets tab)
collectReposts: true,
// Collect users who quote-tweeted (the /quotes tab)
collectQuotes: true,
// Stop after collecting this many entries total (safety cap)
maxEntries: 5000,
// How far to scroll each step (pixels)
scrollStep: 1400,
// Delay between scrolls (ms). Raise to 2500+ on a slow connection so rows load.
scrollDelay: 1500,
// Give up on a tab after this many consecutive scrolls with no new rows
stallLimit: 6,
// Hard cap on scroll attempts per tab, independent of stalls
maxScrollAttempts: 300,
// Also trigger a browser download of the CSV file (JSON always downloads)
autoDownloadCsv: false
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
userCell: '[data-testid="UserCell"]',
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userName: '[data-testid="User-Name"]',
userDescription: '[data-testid="UserDescription"]',
followsYou: '[data-testid="userFollowIndicator"]',
verified: 'svg[aria-label*="Verified"], [data-testid="icon-verified"]',
profileLink: 'a[href^="/"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const randomDelay = () => sleep(CONFIG.scrollDelay + Math.floor(Math.random() * 500));
const log = {
info: (m) => console.log(`ℹ️ ${m}`),
success: (m) => console.log(`✅ ${m}`),
warning: (m) => console.log(`⚠️ ${m}`),
error: (m) => console.log(`❌ ${m}`),
count: (label, n) => console.log(`🔁 Collected ${n} ${label}...`)
};
const csvCell = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const parseNumber = (str) => {
if (!str) return 0;
const s = str.trim().toUpperCase();
const num = parseFloat(s.replace(/,/g, '')) || 0;
if (s.includes('K')) return Math.round(num * 1000);
if (s.includes('M')) return Math.round(num * 1000000);
return Math.round(num);
};
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Download of ${filename} failed: ${e.message}`);
return false;
}
};
// Reposters render as UserCell rows (people).
const extractUser = (cell) => {
const anchors = Array.from(cell.querySelectorAll(SELECTORS.profileLink));
let handle = '';
for (const a of anchors) {
const href = a.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
if (m) { handle = m[1]; break; }
}
if (!handle) {
const at = anchors.find((a) => a.textContent.trim().startsWith('@'));
if (at) handle = at.textContent.trim().slice(1).split(/\s/)[0];
}
if (!handle) return null;
let displayName = '';
const nameAnchor = anchors.find((a) => {
const t = a.textContent.trim();
return t && !t.startsWith('@');
});
displayName = (nameAnchor && nameAnchor.textContent.trim()) ||
(cell.querySelector('div[dir="ltr"] span')?.textContent.trim()) || '';
let bio = cell.querySelector(SELECTORS.userDescription)?.textContent?.trim() || '';
if (!bio) {
const auto = cell.querySelector('[dir="auto"]:not([data-testid])')?.textContent?.trim();
if (auto && auto.length >= 10 && !auto.startsWith('@')) bio = auto;
}
return {
type: 'repost',
handle,
displayName,
bio,
verified: !!cell.querySelector(SELECTORS.verified),
followsYou: !!cell.querySelector(SELECTORS.followsYou),
profileUrl: `https://x.com/${handle}`
};
};
// Quote-tweets render as full article tweets (post + author + counts).
const extractQuote = (article) => {
const timeEl = article.querySelector('time');
const permalink = timeEl?.closest('a[href*="/status/"]') || article.querySelector('a[href*="/status/"]');
const url = permalink ? permalink.href : '';
const quoteId = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
if (!quoteId) return null;
const handle = (url.match(/x\.com\/([^/]+)\/status/) || [])[1] || '';
const nameText = article.querySelector(SELECTORS.userName)?.textContent || '';
const displayName = nameText.split('@')[0].trim();
const verified = !!article.querySelector(`${SELECTORS.userName} ${SELECTORS.verified}`);
const text = article.querySelector(SELECTORS.tweetText)?.textContent || '';
const time = timeEl ? timeEl.getAttribute('datetime') : '';
const getMetric = (testId) => {
const el = article.querySelector(`[data-testid="${testId}"]`);
const span = el?.querySelector('span span');
return parseNumber(span ? span.textContent : '0');
};
const viewsEl = article.querySelector('a[href$="/analytics"]');
const views = parseNumber(viewsEl ? viewsEl.textContent : '0');
return {
type: 'quote',
quoteId,
handle,
displayName,
verified,
text,
url,
time,
replies: getMetric('reply'),
retweets: getMetric('retweet'),
likes: getMetric('like'),
views
};
};
const getTweetId = () => (window.location.pathname.match(/status\/(\d+)/) || [])[1] || null;
// ============================================
// 🛑 STOP SWITCH
// ============================================
let stopped = false;
window.stopScrapeReposters = () => {
stopped = true;
log.warning('Stop requested. Finishing the current scroll, then exporting what was collected.');
};
// ============================================
// 🚦 PAGE GUARD
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔁 SCRAPE REPOSTERS + QUOTERS - XActions ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
const tweetId = getTweetId();
if (!tweetId || !/\/(retweets|quotes)\/?$/.test(window.location.pathname)) {
log.warning('You are not on a post Reposts or Quotes page.');
log.info('Open a post, tap its reposts count, and land on x.com/<user>/status/<id>/retweets (or /quotes), then run this again.');
return;
}
log.info(`Scraping reposters and quoters of post ${tweetId}`);
log.info(`To stop early: window.stopScrapeReposters()`);
// ============================================
// 🎯 COLLECTION (merges across tabs and across runs)
// ============================================
const items = new Map();
const stats = { reposts: 0, quotes: 0, verified: 0, scrolls: 0, errors: 0 };
// Seed from a prior run against the same post so a manual two-tab flow merges.
const prior = window.__scrapeRepostersData;
if (prior && prior.tweetId === tweetId && Array.isArray(prior.entries)) {
prior.entries.forEach((e) => {
const key = e.type === 'quote' ? `quote:${e.quoteId}` : `repost:${e.handle}`;
if (!items.has(key)) items.set(key, e);
});
if (items.size) log.info(`Merged ${items.size} entries from a previous run on this post.`);
}
const harvest = async (kind) => {
const label = kind === 'repost' ? 'reposters' : 'quotes';
let scrollAttempts = 0;
let stall = 0;
while (!stopped && items.size < CONFIG.maxEntries && stall < CONFIG.stallLimit && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = items.size;
if (kind === 'repost') {
document.querySelectorAll(SELECTORS.userCell).forEach((cell) => {
try {
const u = extractUser(cell);
if (u && u.handle) {
const key = `repost:${u.handle}`;
if (!items.has(key)) items.set(key, u);
}
} catch (e) { stats.errors++; }
});
} else {
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
try {
const q = extractQuote(article);
if (q && q.quoteId && q.quoteId !== tweetId) {
const key = `quote:${q.quoteId}`;
if (!items.has(key)) items.set(key, q);
}
} catch (e) { stats.errors++; }
});
}
if (items.size > before) {
log.count(label, items.size);
stall = 0;
} else {
stall++;
}
window.scrollBy(0, CONFIG.scrollStep);
await randomDelay();
scrollAttempts++;
stats.scrolls++;
}
};
// Switch tabs inside the single-page app (no full reload, script keeps running).
const goToTab = async (segment) => {
if (new RegExp(`/${segment}/?$`).test(window.location.pathname)) return true;
const link = Array.from(document.querySelectorAll('a[href*="/status/"]'))
.find((a) => new RegExp(`/status/${tweetId}/${segment}$`).test(a.getAttribute('href') || ''));
if (!link) return false;
link.click();
for (let i = 0; i < 40 && !stopped; i++) {
await sleep(300);
if (new RegExp(`/${segment}/?$`).test(window.location.pathname)) {
await sleep(900); // let the first rows render
return true;
}
}
return new RegExp(`/${segment}/?$`).test(window.location.pathname);
};
// Scrape reposts first, then quotes, whichever tab we started on.
if (CONFIG.collectReposts && !stopped) {
if (await goToTab('retweets')) {
window.scrollTo(0, 0);
await sleep(600);
await harvest('repost');
} else {
log.warning('Could not open the Reposts tab automatically. Open x.com/<user>/status/<id>/retweets and run again to add reposters.');
}
}
if (CONFIG.collectQuotes && !stopped) {
if (await goToTab('quotes')) {
window.scrollTo(0, 0);
await sleep(600);
await harvest('quote');
} else {
log.warning('Could not open the Quotes tab automatically. Open x.com/<user>/status/<id>/quotes and run again to add quote-tweets.');
}
}
const list = Array.from(items.values()).slice(0, CONFIG.maxEntries);
stats.reposts = list.filter((e) => e.type === 'repost').length;
stats.quotes = list.filter((e) => e.type === 'quote').length;
stats.verified = list.filter((e) => e.verified).length;
// ============================================
// 📤 EXPORT
// ============================================
const result = {
tweetId,
scrapedAt: new Date().toISOString(),
total: list.length,
reposts: stats.reposts,
quotes: stats.quotes,
entries: list
};
const buildCsv = () => {
const header = 'Type,Handle,DisplayName,Verified,FollowsYou,Bio,QuoteId,Text,Likes,Retweets,Replies,Views,URL,Time';
const rows = list.map((e) => [
e.type,
csvCell('@' + (e.handle || '')),
csvCell(e.displayName),
!!e.verified,
e.type === 'repost' ? !!e.followsYou : '',
csvCell(e.type === 'repost' ? (e.bio || '') : ''),
csvCell(e.type === 'quote' ? e.quoteId : ''),
csvCell(e.type === 'quote' ? e.text : ''),
e.type === 'quote' ? e.likes : '',
e.type === 'quote' ? e.retweets : '',
e.type === 'quote' ? e.replies : '',
e.type === 'quote' ? e.views : '',
csvCell(e.type === 'quote' ? e.url : (e.profileUrl || '')),
csvCell(e.type === 'quote' ? e.time : '')
].join(','));
return [header, ...rows].join('\n');
};
const csv = buildCsv();
const dateStr = new Date().toISOString().split('T')[0];
// Persist for a possible follow-up run on the other tab.
window.__scrapeRepostersData = { tweetId, entries: list };
if (list.length === 0) {
log.warning('No reposters or quoters were collected. The post may have none, or both tabs were unreachable.');
} else {
download(JSON.stringify(result, null, 2), `tweet_${tweetId}_reposters_${dateStr}.json`, 'application/json');
log.success(`Downloaded tweet_${tweetId}_reposters_${dateStr}.json`);
if (CONFIG.autoDownloadCsv) {
download(csv, `tweet_${tweetId}_reposters_${dateStr}.csv`, 'text/csv');
log.success(`Downloaded tweet_${tweetId}_reposters_${dateStr}.csv`);
}
}
window.scrapedReposters = result;
window.scrapeRepostersExportCSV = () => {
download(csv, `tweet_${tweetId}_reposters_${dateStr}.csv`, 'text/csv');
return csv;
};
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE REPOSTERS + QUOTERS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🔁 Reposters: ${String(stats.reposts).padEnd(30)}║
║ 💬 Quote-tweets: ${String(stats.quotes).padEnd(30)}║
║ ✔️ Verified (either): ${String(stats.verified).padEnd(30)}║
║ 📜 Scrolls: ${String(stats.scrolls).padEnd(30)}║
║ ❌ Row errors: ${String(stats.errors).padEnd(30)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((e, i) => {
if (e.type === 'quote') {
console.log(`${i + 1}. [quote] @${e.handle}${e.verified ? ' ✓' : ''} (${e.likes} ❤️): "${e.text.slice(0, 50)}..."`);
} else {
console.log(`${i + 1}. [repost] @${e.handle}${e.verified ? ' ✓' : ''} - ${e.displayName}`);
}
});
if (list.length > 5) console.log(` ... and ${list.length - 5} more`);
console.log('\n📋 CSV preview (full string via window.scrapeRepostersExportCSV()):');
console.log(csv.split('\n').slice(0, 6).join('\n'));
console.log('\n💡 window.scrapedReposters = full data. window.scrapeRepostersExportCSV() = download + return CSV.');
return result;
})();
});
register("scrape-search", function(){
(async function scrapeSearch() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of posts to collect before stopping
maxPosts: 300,
// Include reply posts in the results (false = original posts only)
includeReplies: true,
// Delay between scrolls (ms). Raise if posts load slowly.
scrollDelay: 1800,
// Give up after this many scrolls that add no new posts
noNewPostsThreshold: 6,
// Hard cap on scroll attempts so the loop can never run forever
maxScrollAttempts: 400,
// Auto-download a CSV file too (JSON always downloads)
downloadCsv: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
primaryColumn: '[data-testid="primaryColumn"]',
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userName: '[data-testid="User-Name"]',
actionGroup: '[role="group"]',
photo: '[data-testid="tweetPhoto"] img',
video: 'video'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (n, total) => console.log(`📊 Collected ${n}/${total} posts...`)
};
// Turn an aria-label like "1,234 Likes. Like" into the number 1234.
const countFromLabel = (label) => {
if (!label) return 0;
const raw = (label.match(/[\d.,]+\s*[KMB]?/i) || ['0'])[0].trim().toUpperCase();
const num = parseFloat(raw.replace(/,/g, '')) || 0;
if (raw.includes('K')) return Math.round(num * 1e3);
if (raw.includes('M')) return Math.round(num * 1e6);
if (raw.includes('B')) return Math.round(num * 1e9);
return Math.round(num);
};
// Read the reply/repost/like counts from the action buttons' aria-labels.
const getCounts = (article) => {
const counts = { replies: 0, reposts: 0, likes: 0, views: 0 };
const group = article.querySelector(SELECTORS.actionGroup);
const nodes = group ? group.querySelectorAll('[aria-label]') : [];
nodes.forEach((node) => {
const label = (node.getAttribute('aria-label') || '').toLowerCase();
const value = countFromLabel(label);
if (label.includes('repl')) counts.replies = value;
else if (label.includes('repost') || label.includes('retweet')) counts.reposts = value;
else if (label.includes('like')) counts.likes = value;
else if (label.includes('view')) counts.views = value;
});
return counts;
};
// Strip the &name= sizing param so image URLs point at the original resolution.
const toOriginalImage = (url) => {
if (!url) return url;
return url.replace(/&name=[^&]*/i, '&name=orig').replace(/\?name=[^&]*/i, '?name=orig');
};
const getMediaUrls = (article) => {
const urls = new Set();
article.querySelectorAll(SELECTORS.photo).forEach((img) => {
if (img.src) urls.add(toOriginalImage(img.src));
});
article.querySelectorAll(SELECTORS.video).forEach((v) => {
if (v.src) urls.add(v.src);
else if (v.poster) urls.add(v.poster);
});
return Array.from(urls);
};
const isReply = (article) => {
if (article.querySelector('[data-testid="in-reply-to"]') !== null) return true;
return Array.from(article.querySelectorAll('div[dir]')).some((el) =>
(el.textContent || '').startsWith('Replying to'));
};
// Build a post object from an article, or null if it has no resolvable id.
const extractPost = (article) => {
// The permalink around the timestamp is the post's own URL; the first
// /status/ link in the article can belong to a quoted post.
const timeEl = article.querySelector('time');
const permalink = timeEl ? timeEl.closest('a[href*="/status/"]') : null;
const anchor = permalink || article.querySelector('a[href*="/status/"]');
const url = anchor ? anchor.href : '';
const id = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
if (!id) return null;
const nameBlock = article.querySelector(SELECTORS.userName);
const nameText = nameBlock ? nameBlock.textContent : '';
const handle = (nameText.match(/@(\w+)/) || [, ''])[1];
const name = nameText.split('@')[0].trim();
const textEl = article.querySelector(SELECTORS.tweetText);
const text = textEl ? textEl.textContent : '';
const counts = getCounts(article);
return {
id,
url,
author: { handle, name },
text,
likes: counts.likes,
reposts: counts.reposts,
replies: counts.replies,
views: counts.views,
timestamp: timeEl ? timeEl.getAttribute('datetime') : null,
mediaUrls: getMediaUrls(article),
isReply: isReply(article),
scrapedAt: new Date().toISOString()
};
};
const downloadFile = (content, filename, mime) => {
try {
const blob = new Blob([content], { type: mime });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
return true;
} catch (e) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const csvCell = (value) => `"${String(value == null ? '' : value).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const toCSV = (posts) => {
const headers = ['id', 'handle', 'name', 'text', 'likes', 'reposts', 'replies', 'views', 'timestamp', 'mediaUrls', 'url'];
const rows = posts.map((p) => [
p.id,
'@' + p.author.handle,
p.author.name,
p.text,
p.likes,
p.reposts,
p.replies,
p.views,
p.timestamp || '',
p.mediaUrls.join(' '),
p.url
].map(csvCell).join(','));
return [headers.join(','), ...rows].join('\n');
};
// ============================================
// 🎯 MAIN
// ============================================
const query = new URLSearchParams(window.location.search).get('q') || 'search';
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🔍 SCRAPE SEARCH - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: warn and return, never hard-redirect (that kills the script).
if (!/\/search/.test(window.location.pathname)) {
log.warning('You are not on a search results page.');
log.info('Go to x.com/search?q=YOUR+TERM&f=live, then run this script again.');
return;
}
const posts = new Map();
// Stop switch: run window.stopScrapeSearch() to end the scroll loop.
let stopped = false;
window.stopScrapeSearch = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
log.info(`Search query: "${query}"`);
log.info(`Max posts: ${CONFIG.maxPosts} | Include replies: ${CONFIG.includeReplies}`);
log.info('To stop early: window.stopScrapeSearch()');
let scrollAttempts = 0;
let noNewCount = 0;
while (!stopped && posts.size < CONFIG.maxPosts && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = posts.size;
const articles = document.querySelectorAll(SELECTORS.tweet);
articles.forEach((article) => {
try {
const post = extractPost(article);
if (!post || posts.has(post.id)) return;
if (!CONFIG.includeReplies && post.isReply) return;
posts.set(post.id, post);
} catch (e) {
log.error(`Skipped a post: ${e.message}`);
}
});
if (posts.size > before) {
log.progress(posts.size, CONFIG.maxPosts);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewPostsThreshold) {
log.warning('No new posts after several scrolls. Reached the end of the results.');
break;
}
}
if (posts.size >= CONFIG.maxPosts) break;
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
const list = Array.from(posts.values()).slice(0, CONFIG.maxPosts);
// ============================================
// 📊 SUMMARY + EXPORT
// ============================================
if (list.length === 0) {
log.warning('No posts were found. The results may be empty, or the page had not loaded yet.');
return;
}
const totalLikes = list.reduce((s, p) => s + p.likes, 0);
const totalReposts = list.reduce((s, p) => s + p.reposts, 0);
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE SEARCH - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🔍 Query: ${String(query.slice(0, 36)).padEnd(37)}║
║ 📝 Posts: ${String(list.length).padEnd(37)}║
║ ❤️ Total likes: ${String(totalLikes.toLocaleString()).padEnd(37)}║
║ 🔄 Total reposts:${String(totalReposts.toLocaleString()).padEnd(37)}║
╚══════════════════════════════════════════════════════════╝
`);
list.slice(0, 5).forEach((p, i) => {
console.log(`${i + 1}. @${p.author.handle} (${p.likes} ❤️): "${p.text.slice(0, 60)}"`);
});
const safeQuery = query.replace(/[^a-z0-9]/gi, '_').slice(0, 30) || 'search';
const stamp = new Date().toISOString().split('T')[0];
const result = { query, scrapedAt: new Date().toISOString(), count: list.length, posts: list };
if (downloadFile(JSON.stringify(result, null, 2), `search_${safeQuery}_${stamp}.json`, 'application/json')) {
log.success('JSON downloaded');
}
if (CONFIG.downloadCsv && downloadFile(toCSV(list), `search_${safeQuery}_${stamp}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
window.scrapedSearch = result;
window.exportCSV = () => toCSV(list);
console.log('');
log.info('window.scrapedSearch holds the full data. window.exportCSV() returns the CSV string.');
log.success('Done! by nichxbt');
return result;
})();
});
register("scrape-spaces", function(){
(async function scrapeSpaces() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of Space cards to collect when listing
maxSpaces: 50,
// Maximum scroll attempts when listing Spaces on a profile
maxScrollAttempts: 60,
// Stop after this many scrolls with no new Spaces
noNewThreshold: 4,
// Delay between scrolls (ms)
scrollDelay: 1500,
// Auto-download the results as files when finished
downloadJSON: true,
downloadCSV: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
// Markers that we are INSIDE a Space (listening/hosting)
inSpace: '[data-testid="spaceSpeakers"], [data-testid="audioSpaceInlineTitle"], [aria-label="Space"]',
// Space title inside an open Space
spaceTitle: '[data-testid="audioSpaceInlineTitle"], [data-testid="spaceTitle"]',
// Speaker/participant cells
userCell: '[data-testid="UserCell"]',
// A Space card / link anywhere in the timeline
spaceCard: 'a[href*="/i/spaces/"], [data-testid="placementTracking"] a[href*="/spaces/"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Failed to download ${filename}: ${e.message}`);
return false;
}
};
const handleFromLink = (link) => {
if (!link) return '';
const href = link.getAttribute('href') || '';
const m = href.match(/^\/([A-Za-z0-9_]{1,15})$/);
return m ? m[1] : '';
};
// ============================================
// 🎯 MAIN
// ============================================
let stopped = false;
window.stopScrapeSpaces = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then exporting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🎙️ SCRAPE SPACES - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
const insideSpace = document.querySelector(SELECTORS.inSpace) !== null;
// ------------------------------------------------------------------
// MODE A: inside a live/open Space -> capture its metadata + speakers
// ------------------------------------------------------------------
if (insideSpace) {
log.info('Detected an open Space. Capturing metadata and speakers...');
const titleEl = document.querySelector(SELECTORS.spaceTitle);
const title = titleEl ? titleEl.textContent.trim() : '';
// Listener count: X renders it as text like "1,234 listening". Find the
// nearest element whose text mentions listening/tuned in.
let listeners = '';
const candidates = document.querySelectorAll('span, div');
for (const el of candidates) {
const t = el.textContent.trim();
if (/^[\d,\.KMB]+\s+(listening|listeners|tuned in)/i.test(t)) { listeners = t; break; }
}
const speakers = [];
document.querySelectorAll(SELECTORS.userCell).forEach(cell => {
try {
const handle = handleFromLink(cell.querySelector('a[href^="/"]'));
const nameEl = cell.querySelector('[dir="ltr"] span, [dir="ltr"]');
const name = nameEl ? nameEl.textContent.trim() : '';
if (!handle) return;
if (!speakers.find(s => s.handle === handle)) {
speakers.push({ handle, name });
}
} catch (e) {
// Skip malformed cell
}
});
const result = {
mode: 'space',
url: window.location.href,
title,
listeners,
host: speakers.length > 0 ? speakers[0].handle : '',
speakers,
scrapedAt: new Date().toISOString()
};
console.log('');
console.log(`📋 Title: ${title || '(not found)'}`);
console.log(`👥 Listeners: ${listeners || '(not found)'}`);
console.log(`🎤 Speakers captured: ${speakers.length}`);
if (speakers.length > 0) {
speakers.slice(0, 10).forEach((s, i) => console.log(` ${i + 1}. @${s.handle}${s.name ? ` (${s.name})` : ''}`));
}
const dateStr = new Date().toISOString().split('T')[0];
console.log('');
if (CONFIG.downloadJSON) {
if (download(JSON.stringify(result, null, 2), `space_${dateStr}.json`, 'application/json')) {
log.success('JSON downloaded');
}
}
if (CONFIG.downloadCSV) {
const esc = (v) => `"${String(v).replace(/"/g, '""').replace(/\n/g, ' ')}"`;
const csv = [
['Handle', 'Name'].join(','),
...speakers.map(s => [esc(s.handle), esc(s.name)].join(','))
].join('\n');
if (download(csv, `space_speakers_${dateStr}.csv`, 'text/csv')) {
log.success('CSV (speakers) downloaded');
}
}
window.scrapedSpace = result;
console.log('\n💡 Access your data: window.scrapedSpace');
console.log('✅ Script completed! by nichxbt');
return result;
}
// ------------------------------------------------------------------
// MODE B: on a profile / timeline -> list Space cards
// ------------------------------------------------------------------
log.info('No open Space detected. Listing Space cards from the page...');
log.info(`Max Spaces: ${CONFIG.maxSpaces}`);
log.info('To stop early: window.stopScrapeSpaces()');
console.log('');
const spaces = new Map();
let scrollAttempts = 0;
let noNewCount = 0;
const collectCards = () => {
document.querySelectorAll(SELECTORS.spaceCard).forEach(card => {
try {
const link = card.href || (card.querySelector('a[href*="/spaces/"]') || {}).href || '';
if (!link) return;
const idMatch = link.match(/\/spaces\/([A-Za-z0-9]+)/);
const id = idMatch ? idMatch[1] : link;
if (spaces.has(id)) return;
const container = card.closest('article, [data-testid="cellInnerDiv"]') || card;
const text = (container.textContent || '').trim();
const lower = text.toLowerCase();
const isLive = lower.includes('live') && !lower.includes('recorded');
// Listener/participant count if present in the card text
const listenersMatch = text.match(/([\d,\.KMB]+)\s+(listening|listeners|tuned in|played)/i);
const listeners = listenersMatch ? listenersMatch[0] : '';
// Host handle: first bare profile link in the card container
let host = '';
for (const a of container.querySelectorAll('a[href^="/"]')) {
const h = handleFromLink(a);
if (h) { host = h; break; }
}
spaces.set(id, {
id,
title: text.slice(0, 140),
host,
listeners,
isLive,
link: link.startsWith('http') ? link : `https://x.com${link}`
});
} catch (e) {
// Skip malformed card
}
});
};
while (!stopped && spaces.size < CONFIG.maxSpaces && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = spaces.size;
collectCards();
const added = spaces.size - before;
if (added > 0) {
log.info(`Collected ${spaces.size} Spaces (+${added})`);
noNewCount = 0;
} else {
noNewCount++;
if (noNewCount >= CONFIG.noNewThreshold) {
break;
}
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
}
collectCards();
const list = Array.from(spaces.values());
console.log('');
console.log('╔══════════════════════════════════════════════════════════╗');
console.log('║ 📊 SCRAPE SPACES - COMPLETE ║');
console.log('╚══════════════════════════════════════════════════════════╝');
console.log(`🎙️ Total Spaces: ${list.length}`);
console.log(`📜 Scroll attempts: ${scrollAttempts}`);
if (list.length === 0) {
log.warning('No Space cards found on this page.');
log.info('Open a profile that hosts Spaces (scroll to a Space card), or open a Space directly, then rerun.');
return { spaces: [] };
}
list.forEach((s, i) => {
console.log(` ${s.isLive ? '🔴' : '⏳'} ${i + 1}. ${s.title.slice(0, 70)}${s.listeners ? ` (${s.listeners})` : ''}`);
});
const dateStr = new Date().toISOString().split('T')[0];
const result = { mode: 'listing', count: list.length, spaces: list, scrapedAt: new Date().toISOString() };
console.log('');
if (CONFIG.downloadJSON) {
if (download(JSON.stringify(result, null, 2), `spaces_${dateStr}.json`, 'application/json')) {
log.success('JSON downloaded');
}
}
if (CONFIG.downloadCSV) {
const esc = (v) => `"${String(v).replace(/"/g, '""').replace(/\n/g, ' ')}"`;
const csv = [
['Title', 'Host', 'Listeners', 'IsLive', 'Link'].join(','),
...list.map(s => [esc(s.title), esc(s.host), esc(s.listeners), s.isLive, esc(s.link)].join(','))
].join('\n');
if (download(csv, `spaces_${dateStr}.csv`, 'text/csv')) {
log.success('CSV downloaded');
}
}
window.scrapedSpaces = result;
console.log('\n💡 Access your data: window.scrapedSpaces');
console.log('✅ Script completed! by nichxbt');
return result;
})();
});
register("scrape-user-likes", function(){
(async function scrapeUserLikes() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Stop after collecting this many liked posts (safety cap)
maxPosts: 1000,
// How far to scroll each step (pixels)
scrollStep: 1600,
// Delay between scrolls (ms). Raise to 2500+ on a slow connection so posts load.
scrollDelay: 1600,
// Give up after this many consecutive scrolls with no new posts (end of timeline)
stallLimit: 6,
// Hard cap on scroll attempts, independent of stalls
maxScrollAttempts: 400,
// Also trigger a browser download of the CSV file (JSON always downloads)
autoDownloadCsv: false
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
userName: '[data-testid="User-Name"]',
verified: 'svg[aria-label*="Verified"], [data-testid="icon-verified"]',
photo: '[data-testid="tweetPhoto"]',
video: '[data-testid="videoPlayer"], [data-testid="videoComponent"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const randomDelay = () => sleep(CONFIG.scrollDelay + Math.floor(Math.random() * 500));
const log = {
info: (m) => console.log(`ℹ️ ${m}`),
success: (m) => console.log(`✅ ${m}`),
warning: (m) => console.log(`⚠️ ${m}`),
error: (m) => console.log(`❌ ${m}`),
count: (n) => console.log(`💛 Collected ${n} liked posts...`)
};
const csvCell = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""').replace(/\r?\n/g, ' ')}"`;
const parseNumber = (str) => {
if (!str) return 0;
const s = str.trim().toUpperCase();
const num = parseFloat(s.replace(/,/g, '')) || 0;
if (s.includes('K')) return Math.round(num * 1000);
if (s.includes('M')) return Math.round(num * 1000000);
return Math.round(num);
};
const download = (content, filename, type) => {
try {
const blob = new Blob([content], { type });
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) {
log.error(`Download of ${filename} failed: ${e.message}`);
return false;
}
};
const extractPost = (article) => {
// The permalink around the timestamp is the post's own URL (not a quoted one).
const timeEl = article.querySelector('time');
const permalink = timeEl?.closest('a[href*="/status/"]') || article.querySelector('a[href*="/status/"]');
const url = permalink ? permalink.href : '';
const id = url ? (url.split('/status/')[1] || '').split(/[?/]/)[0] : '';
if (!id) return null;
// Author handle is reliable from the permalink path: /<handle>/status/<id>
const handle = (url.match(/x\.com\/([^/]+)\/status/) || [])[1] || '';
const nameText = article.querySelector(SELECTORS.userName)?.textContent || '';
const displayName = nameText.split('@')[0].trim();
const verified = !!article.querySelector(`${SELECTORS.userName} ${SELECTORS.verified}`);
const text = article.querySelector(SELECTORS.tweetText)?.textContent || '';
const time = timeEl ? timeEl.getAttribute('datetime') : '';
const getMetric = (testId) => {
const el = article.querySelector(`[data-testid="${testId}"]`);
const span = el?.querySelector('span span');
return parseNumber(span ? span.textContent : '0');
};
const viewsEl = article.querySelector('a[href$="/analytics"]');
const views = parseNumber(viewsEl ? viewsEl.textContent : '0');
const hasImage = article.querySelector(SELECTORS.photo) !== null;
const hasVideo = article.querySelector(SELECTORS.video) !== null;
return {
id,
url,
author: { handle, displayName, verified },
text,
time,
counts: {
likes: getMetric('like'),
retweets: getMetric('retweet'),
replies: getMetric('reply'),
views
},
hasMedia: hasImage || hasVideo
};
};
// ============================================
// 🛑 STOP SWITCH
// ============================================
let stopped = false;
window.stopScrapeUserLikes = () => {
stopped = true;
log.warning('Stop requested. Finishing the current scroll, then exporting what was collected.');
};
// ============================================
// 🚦 PAGE GUARD
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 💛 SCRAPE USER LIKES - XActions ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
if (!/\/likes\/?$/.test(window.location.pathname) || /\/status\//.test(window.location.pathname)) {
log.warning('You are not on a profile Likes page.');
log.info('Go to x.com/<user>/likes (the Likes tab of a profile), then run this again.');
return;
}
const username = window.location.pathname.split('/')[1] || 'unknown';
log.info(`Scraping liked posts of @${username}`);
log.info(`Max posts: ${CONFIG.maxPosts}. To stop early: window.stopScrapeUserLikes()`);
// ============================================
// 🎯 MAIN LOOP
// ============================================
const posts = new Map();
const stats = { collected: 0, withMedia: 0, scrolls: 0, errors: 0 };
let scrollAttempts = 0;
let stall = 0;
while (!stopped && posts.size < CONFIG.maxPosts && stall < CONFIG.stallLimit && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = posts.size;
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
try {
const p = extractPost(article);
if (p && p.id && !posts.has(p.id)) posts.set(p.id, p);
} catch (e) {
stats.errors++;
}
});
if (posts.size > before) {
log.count(posts.size);
stall = 0;
} else {
stall++;
}
window.scrollBy(0, CONFIG.scrollStep);
await randomDelay();
scrollAttempts++;
stats.scrolls = scrollAttempts;
}
const list = Array.from(posts.values()).slice(0, CONFIG.maxPosts);
stats.collected = list.length;
stats.withMedia = list.filter((p) => p.hasMedia).length;
// Most-liked accounts
const accountCounts = {};
list.forEach((p) => {
const h = p.author.handle || 'unknown';
accountCounts[h] = (accountCounts[h] || 0) + 1;
});
const topAccounts = Object.entries(accountCounts).sort((a, b) => b[1] - a[1]).slice(0, 5);
// ============================================
// 📤 EXPORT
// ============================================
const result = {
profile: username,
profileUrl: `https://x.com/${username}`,
scrapedAt: new Date().toISOString(),
total: list.length,
topLikedAccounts: topAccounts.map(([handle, count]) => ({ handle, count })),
posts: list
};
const buildCsv = () => {
const header = 'AuthorHandle,AuthorName,Verified,Text,Likes,Retweets,Replies,Views,HasMedia,URL,Time';
const rows = list.map((p) => [
csvCell('@' + p.author.handle),
csvCell(p.author.displayName),
p.author.verified,
csvCell(p.text),
p.counts.likes,
p.counts.retweets,
p.counts.replies,
p.counts.views,
p.hasMedia,
csvCell(p.url),
csvCell(p.time)
].join(','));
return [header, ...rows].join('\n');
};
const csv = buildCsv();
const dateStr = new Date().toISOString().split('T')[0];
if (list.length === 0) {
log.warning('No liked posts were collected. The Likes tab may be empty or hidden (X hides it for other users).');
} else {
download(JSON.stringify(result, null, 2), `${username}_likes_${dateStr}.json`, 'application/json');
log.success(`Downloaded ${username}_likes_${dateStr}.json`);
if (CONFIG.autoDownloadCsv) {
download(csv, `${username}_likes_${dateStr}.csv`, 'text/csv');
log.success(`Downloaded ${username}_likes_${dateStr}.csv`);
}
}
window.scrapedUserLikes = result;
window.scrapeUserLikesExportCSV = () => {
download(csv, `${username}_likes_${dateStr}.csv`, 'text/csv');
return csv;
};
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 SCRAPE USER LIKES - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 💛 Liked posts: ${String(stats.collected).padEnd(30)}║
║ 🖼️ With media: ${String(stats.withMedia).padEnd(30)}║
║ 📜 Scrolls: ${String(stats.scrolls).padEnd(30)}║
║ ❌ Row errors: ${String(stats.errors).padEnd(30)}║
╚══════════════════════════════════════════════════════════╝
`);
if (topAccounts.length > 0) {
console.log('📊 Most-liked accounts:');
topAccounts.forEach(([handle, count]) => console.log(` @${handle}: ${count}`));
}
list.slice(0, 5).forEach((p, i) => {
console.log(`${i + 1}. @${p.author.handle} (${p.counts.likes} ❤️): "${p.text.slice(0, 50)}..."`);
});
if (list.length > 5) console.log(` ... and ${list.length - 5} more`);
console.log('\n📋 CSV preview (full string via window.scrapeUserLikesExportCSV()):');
console.log(csv.split('\n').slice(0, 6).join('\n'));
console.log('\n💡 window.scrapedUserLikes = full data. window.scrapeUserLikesExportCSV() = download + return CSV.');
return result;
})();
});
register("scraper-toolbox", function(){
(function xactionsScraperToolbox() {
'use strict';
// Re-pasting the script replaces any live instance cleanly
if (window.XActionsToolbox && typeof window.XActionsToolbox.destroy === 'function') {
try { window.XActionsToolbox.destroy(); } catch (e) { /* ignore */ }
}
const VERSION = '1.0.0';
const LS_KEY = 'xactions_toolbox_config_v1';
const TAG = '[XActions Toolbox]';
// ==========================================
// CONFIG (edited live from the panel; persisted in localStorage)
// ==========================================
const DEFAULT_CONFIG = {
targetCount: 300, // stop after this many MATCHING tweets (0 = no limit)
scrollDelay: 1800, // ms between scrolls
maxScrolls: 400, // hard cap on scroll attempts
stallLimit: 8, // stop after N scrolls with nothing new
autoDownloadJson: false, // download JSON automatically when a run finishes
filters: {
includeKeywords: [], // keep only tweets containing at least one (empty = all)
excludeKeywords: [], // drop tweets containing any
onlyUsers: [], // keep only tweets authored by these handles (empty = all)
excludeUsers: [], // drop tweets authored by (or reposted from) these handles
minLikes: 0,
minRetweets: 0,
minViews: 0,
daysBack: 0, // 0 = no date limit
excludeRetweets: false,
excludeReplies: false,
excludeQuotes: false,
excludePinned: false,
mediaFilter: 'all', // 'all' | 'with-media' | 'without-media'
lang: '' // BCP-47 code from the tweet payload, e.g. 'en' (empty = all)
},
ui: { top: 80, left: null } // panel position (left null = stick to right edge)
};
function loadConfig() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return structuredClone(DEFAULT_CONFIG);
const saved = JSON.parse(raw);
return {
...structuredClone(DEFAULT_CONFIG),
...saved,
filters: { ...structuredClone(DEFAULT_CONFIG.filters), ...(saved.filters || {}) },
ui: { ...structuredClone(DEFAULT_CONFIG.ui), ...(saved.ui || {}) }
};
} catch (e) {
return structuredClone(DEFAULT_CONFIG);
}
}
function saveConfig() {
try { localStorage.setItem(LS_KEY, JSON.stringify(config)); } catch (e) { /* storage full/blocked */ }
}
const config = loadConfig();
// ==========================================
// STATE
// ==========================================
const state = {
status: 'idle', // idle | running | paused | done
startedAt: null,
elapsedBefore: 0, // accumulated ms across pause cycles
scrolls: 0,
stalls: 0,
lastChangeCount: 0,
destroyed: false
};
/** id -> normalized tweet record. Map preserves capture order. */
const store = new Map();
// ==========================================
// SMALL UTILITIES
// ==========================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function csvList(str) {
return String(str || '')
.split(',')
.map((s) => s.trim().replace(/^@/, ''))
.filter(Boolean);
}
function escapeHtml(s) {
return String(s ?? '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function csvCell(v) {
const s = String(v ?? '');
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}
function parseApprox(str) {
if (!str) return 0;
const s = String(str).trim().toUpperCase().replace(/,/g, '');
const n = parseFloat(s);
if (Number.isNaN(n)) return 0;
if (s.includes('K')) return Math.round(n * 1e3);
if (s.includes('M')) return Math.round(n * 1e6);
if (s.includes('B')) return Math.round(n * 1e9);
return Math.round(n);
}
function fmtInt(n) { return Number(n || 0).toLocaleString(); }
function fmtElapsed(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return m > 0 ? `${m}m ${s % 60}s` : `${s}s`;
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
// Clipboard API needs page focus; fall back to a hidden textarea
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
ta.remove();
return ok;
} catch (e2) {
return false;
}
}
}
function downloadFile(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function pageLabel() {
const path = location.pathname.replace(/^\/+|\/+$/g, '');
if (!path || path === 'home') return 'home';
if (path.startsWith('search')) return 'search';
if (path.startsWith('i/bookmarks')) return 'bookmarks';
return path.split('/')[0];
}
// ==========================================
// GRAPHQL INTERCEPTION
// ==========================================
// The X web app fetches timelines from /i/api/graphql/<hash>/<Operation>.
// We wrap fetch (and XHR as a safety net), clone matching responses, and
// walk the JSON for Tweet objects. No extra requests are ever made; we
// only read what the page already downloaded.
const GQL_URL = /\/i\/api\/graphql\//;
const origFetch = window.fetch;
const origXhrOpen = XMLHttpRequest.prototype.open;
const origXhrSend = XMLHttpRequest.prototype.send;
function installInterceptor() {
window.fetch = async function patchedFetch(...args) {
const res = await origFetch.apply(this, args);
try {
const url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
if (GQL_URL.test(url)) {
res.clone().json().then(ingestGraphql).catch(() => { /* not JSON */ });
}
} catch (e) { /* never break the page's own fetch */ }
return res;
};
XMLHttpRequest.prototype.open = function patchedOpen(method, url, ...rest) {
this.__xatUrl = url;
return origXhrOpen.call(this, method, url, ...rest);
};
XMLHttpRequest.prototype.send = function patchedSend(...args) {
if (this.__xatUrl && GQL_URL.test(String(this.__xatUrl))) {
this.addEventListener('load', () => {
try { ingestGraphql(JSON.parse(this.responseText)); } catch (e) { /* not JSON */ }
});
}
return origXhrSend.apply(this, args);
};
}
function removeInterceptor() {
window.fetch = origFetch;
XMLHttpRequest.prototype.open = origXhrOpen;
XMLHttpRequest.prototype.send = origXhrSend;
}
/**
* Recursively collect Tweet result objects from a GraphQL payload.
* Subtrees under a promoted entry (ads) are skipped entirely.
* Recursion stops at each Tweet node; nested quoted/reposted tweets are
* handled inside normalizeGraphqlTweet so they are not double-counted
* as standalone timeline items.
*/
function collectTweetNodes(node, out) {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node)) { for (const item of node) collectTweetNodes(item, out); return; }
if (typeof node.entryId === 'string' && node.entryId.includes('promoted')) return;
const candidate = node.__typename === 'TweetWithVisibilityResults' ? node.tweet : node;
if (candidate && candidate.rest_id && candidate.legacy && typeof candidate.legacy.full_text === 'string') {
out.push(candidate);
return;
}
for (const key of Object.keys(node)) collectTweetNodes(node[key], out);
}
function userFromResult(userResult) {
const u = (userResult && (userResult.result || userResult)) || null;
if (!u) return { handle: '', name: '', verified: false, followers: null };
const legacy = u.legacy || {};
const core = u.core || {};
return {
handle: legacy.screen_name || core.screen_name || '',
name: legacy.name || core.name || '',
verified: !!u.is_blue_verified,
followers: typeof legacy.followers_count === 'number' ? legacy.followers_count : null
};
}
function normalizeGraphqlTweet(raw, opts = {}) {
const legacy = raw.legacy || {};
const author = userFromResult(raw.core && raw.core.user_results);
// Long posts store the untruncated body in note_tweet
const noteText = raw.note_tweet && raw.note_tweet.note_tweet_results &&
raw.note_tweet.note_tweet_results.result && raw.note_tweet.note_tweet_results.result.text;
const text = noteText || legacy.full_text || '';
const mediaEntities = (legacy.extended_entities && legacy.extended_entities.media) ||
(legacy.entities && legacy.entities.media) || [];
const media = mediaEntities.map((m) => {
const entry = { type: m.type, url: m.media_url_https || m.url || '' };
if (m.video_info && Array.isArray(m.video_info.variants)) {
const best = m.video_info.variants
.filter((v) => v.content_type === 'video/mp4')
.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0];
if (best) entry.videoUrl = best.url;
}
return entry;
});
const rtRaw = legacy.retweeted_status_result && legacy.retweeted_status_result.result;
const rtNode = rtRaw && (rtRaw.__typename === 'TweetWithVisibilityResults' ? rtRaw.tweet : rtRaw);
const retweetOf = rtNode ? normalizeGraphqlTweet(rtNode, { nested: true }) : null;
const qRaw = raw.quoted_status_result && raw.quoted_status_result.result;
const qNode = qRaw && (qRaw.__typename === 'TweetWithVisibilityResults' ? qRaw.tweet : qRaw);
const quoted = (!opts.nested && qNode && qNode.legacy)
? normalizeGraphqlTweet(qNode, { nested: true })
: null;
const record = {
id: raw.rest_id,
url: author.handle ? `https://x.com/${author.handle}/status/${raw.rest_id}` : `https://x.com/i/status/${raw.rest_id}`,
author,
text,
lang: legacy.lang || '',
createdAt: legacy.created_at ? new Date(legacy.created_at).toISOString() : null,
metrics: {
likes: legacy.favorite_count || 0,
retweets: legacy.retweet_count || 0,
replies: legacy.reply_count || 0,
quotes: legacy.quote_count || 0,
bookmarks: legacy.bookmark_count || 0,
views: raw.views && raw.views.count ? parseInt(raw.views.count, 10) : 0
},
media,
type: {
isRetweet: !!retweetOf,
isReply: !!legacy.in_reply_to_status_id_str,
isQuote: !!legacy.is_quote_status && !retweetOf,
isPinned: false // set from the DOM sweep when visible
},
entities: {
hashtags: ((legacy.entities && legacy.entities.hashtags) || []).map((h) => '#' + h.text),
mentions: ((legacy.entities && legacy.entities.user_mentions) || []).map((m) => '@' + m.screen_name),
urls: ((legacy.entities && legacy.entities.urls) || []).map((u) => u.expanded_url || u.url).filter(Boolean)
},
sensitive: !!legacy.possibly_sensitive,
source: 'graphql',
approx: false,
scrapedAt: new Date().toISOString()
};
if (retweetOf) record.retweetOf = { id: retweetOf.id, author: retweetOf.author, url: retweetOf.url, text: retweetOf.text };
if (quoted) record.quoted = { id: quoted.id, author: quoted.author, url: quoted.url, text: quoted.text };
return record;
}
function ingestGraphql(payload) {
if (state.destroyed) return;
const nodes = [];
try { collectTweetNodes(payload, nodes); } catch (e) { return; }
let added = 0;
for (const node of nodes) {
try {
const rec = normalizeGraphqlTweet(node);
if (!rec.id || (!rec.text && rec.media.length === 0)) continue;
const existing = store.get(rec.id);
if (!existing || existing.source === 'dom') {
if (existing && existing.type.isPinned) rec.type.isPinned = true;
store.set(rec.id, rec);
added++;
}
} catch (e) { /* one malformed node never aborts the batch */ }
}
if (added > 0) {
updateStats();
previewLatest();
}
}
// ==========================================
// DOM SWEEP (fallback for tweets rendered before injection)
// ==========================================
function domSweep() {
const articles = document.querySelectorAll('article[data-testid="tweet"]');
let added = 0;
articles.forEach((tweet) => {
try {
const timeEl = tweet.querySelector('time');
if (!timeEl) return;
const link = timeEl.closest('a[href*="/status/"]');
if (!link) return;
const match = link.href.match(/\/([^/]+)\/status\/(\d+)/);
if (!match) return;
const [, handle, id] = match;
const socialContext = tweet.querySelector('[data-testid="socialContext"]');
const isRetweet = !!socialContext && socialContext.closest('a') !== null;
const isPinned = !!socialContext && !isRetweet;
const existing = store.get(id);
if (existing) {
// GraphQL records can't see pinned status; the DOM can
if (isPinned && !existing.type.isPinned) { existing.type.isPinned = true; }
return;
}
const textEl = tweet.querySelector('[data-testid="tweetText"]');
const text = textEl ? textEl.innerText : '';
const metric = (testId) => {
const el = tweet.querySelector(`[data-testid="${testId}"]`);
const span = el && el.querySelector('span span');
return parseApprox(span ? span.innerText : '0');
};
const viewsEl = tweet.querySelector('a[href*="/analytics"]');
const nameEl = tweet.querySelector('[data-testid="User-Name"]');
store.set(id, {
id,
url: `https://x.com/${handle}/status/${id}`,
author: {
handle,
name: nameEl ? nameEl.innerText.split('\n')[0] : handle,
verified: !!tweet.querySelector('[data-testid="icon-verified"]'),
followers: null
},
text,
lang: '',
createdAt: timeEl.getAttribute('datetime'),
metrics: {
likes: metric('like') || metric('unlike'),
retweets: metric('retweet') || metric('unretweet'),
replies: metric('reply'),
quotes: 0,
bookmarks: 0,
views: parseApprox(viewsEl ? viewsEl.innerText : '0')
},
media: [],
type: {
isRetweet,
isReply: false,
isQuote: !!tweet.querySelector('div[role="link"] time'),
isPinned
},
entities: {
hashtags: (text.match(/#[\w]+/g) || []),
mentions: (text.match(/@[\w]+/g) || []),
urls: (text.match(/https?:\/\/[^\s]+/g) || [])
},
sensitive: false,
source: 'dom',
approx: true,
scrapedAt: new Date().toISOString()
});
added++;
} catch (e) { /* skip malformed article */ }
});
if (added > 0) { updateStats(); previewLatest(); }
return added;
}
// ==========================================
// FILTERS
// ==========================================
function matchesFilters(t) {
const f = config.filters;
const text = (t.text || '').toLowerCase();
const handle = (t.author.handle || '').toLowerCase();
if (f.onlyUsers.length && !f.onlyUsers.some((u) => u.toLowerCase() === handle)) return false;
if (f.excludeUsers.length) {
const rtHandle = t.retweetOf ? (t.retweetOf.author.handle || '').toLowerCase() : '';
if (f.excludeUsers.some((u) => {
const lower = u.toLowerCase();
return lower === handle || (rtHandle && lower === rtHandle);
})) return false;
}
if (f.includeKeywords.length && !f.includeKeywords.some((k) => text.includes(k.toLowerCase()))) return false;
if (f.excludeKeywords.length && f.excludeKeywords.some((k) => text.includes(k.toLowerCase()))) return false;
if (f.daysBack > 0 && t.createdAt) {
const cutoff = Date.now() - f.daysBack * 86400000;
if (new Date(t.createdAt).getTime() < cutoff) return false;
}
if (t.metrics.likes < f.minLikes) return false;
if (t.metrics.retweets < f.minRetweets) return false;
if (f.minViews > 0 && t.metrics.views < f.minViews) return false;
if (f.excludeRetweets && t.type.isRetweet) return false;
if (f.excludeReplies && t.type.isReply) return false;
if (f.excludeQuotes && t.type.isQuote) return false;
if (f.excludePinned && t.type.isPinned) return false;
if (f.mediaFilter === 'with-media' && t.media.length === 0) return false;
if (f.mediaFilter === 'without-media' && t.media.length > 0) return false;
if (f.lang && t.lang && t.lang !== f.lang) return false;
return true;
}
function allTweets() { return Array.from(store.values()); }
function matchedTweets() {
return allTweets()
.filter(matchesFilters)
.sort((a, b) => {
const ta = a.createdAt ? Date.parse(a.createdAt) : 0;
const tb = b.createdAt ? Date.parse(b.createdAt) : 0;
return tb - ta;
});
}
// ==========================================
// EXPORTS
// ==========================================
function buildStats(tweets) {
const stats = {
total: tweets.length,
totalLikes: 0, totalRetweets: 0, totalReplies: 0, totalViews: 0,
avgLikes: 0, avgRetweets: 0,
withMedia: 0, retweets: 0, replies: 0, quotes: 0,
topHashtags: {}, topMentions: {}
};
for (const t of tweets) {
stats.totalLikes += t.metrics.likes;
stats.totalRetweets += t.metrics.retweets;
stats.totalReplies += t.metrics.replies;
stats.totalViews += t.metrics.views;
if (t.media.length) stats.withMedia++;
if (t.type.isRetweet) stats.retweets++;
if (t.type.isReply) stats.replies++;
if (t.type.isQuote) stats.quotes++;
for (const h of t.entities.hashtags) stats.topHashtags[h.toLowerCase()] = (stats.topHashtags[h.toLowerCase()] || 0) + 1;
for (const m of t.entities.mentions) stats.topMentions[m.toLowerCase()] = (stats.topMentions[m.toLowerCase()] || 0) + 1;
}
if (tweets.length) {
stats.avgLikes = Math.round(stats.totalLikes / tweets.length);
stats.avgRetweets = Math.round(stats.totalRetweets / tweets.length);
}
return stats;
}
function exportBundle() {
const tweets = matchedTweets();
return {
source: pageLabel(),
pageUrl: location.href,
scrapedAt: new Date().toISOString(),
toolboxVersion: VERSION,
filters: config.filters,
totalCaptured: store.size,
totalMatched: tweets.length,
statistics: buildStats(tweets),
tweets
};
}
function toJSON() { return JSON.stringify(exportBundle(), null, 2); }
function toCSV() {
const headers = ['id', 'date', 'author', 'text', 'likes', 'retweets', 'replies', 'quotes', 'bookmarks', 'views',
'lang', 'isRetweet', 'isReply', 'isQuote', 'isPinned', 'mediaCount', 'mediaUrls', 'hashtags', 'url', 'source'];
const rows = matchedTweets().map((t) => [
t.id,
t.createdAt || '',
'@' + t.author.handle,
csvCell(t.text),
t.metrics.likes, t.metrics.retweets, t.metrics.replies, t.metrics.quotes, t.metrics.bookmarks, t.metrics.views,
t.lang,
t.type.isRetweet, t.type.isReply, t.type.isQuote, t.type.isPinned,
t.media.length,
csvCell(t.media.map((m) => m.videoUrl || m.url).join(' ')),
csvCell(t.entities.hashtags.join(' ')),
t.url,
t.source
].join(','));
return [headers.join(','), ...rows].join('\n');
}
function toMarkdown() {
const bundle = exportBundle();
const s = bundle.statistics;
let md = `# X posts export (${bundle.source})\n\n`;
md += `> Scraped ${bundle.scrapedAt} from ${bundle.pageUrl}\n`;
md += `> ${bundle.totalMatched} matching posts (of ${bundle.totalCaptured} captured)\n\n`;
md += `## Statistics\n\n| Metric | Value |\n|--------|-------|\n`;
md += `| Total likes | ${fmtInt(s.totalLikes)} |\n| Total reposts | ${fmtInt(s.totalRetweets)} |\n`;
md += `| Total views | ${fmtInt(s.totalViews)} |\n| Avg likes | ${fmtInt(s.avgLikes)} |\n| With media | ${s.withMedia} |\n\n`;
md += `## Posts\n\n`;
for (const [i, t] of bundle.tweets.entries()) {
md += `### ${i + 1}. @${t.author.handle} (${t.createdAt ? t.createdAt.slice(0, 10) : 'unknown date'})\n\n`;
md += `${t.text}\n\n`;
md += `Likes ${fmtInt(t.metrics.likes)} | Reposts ${fmtInt(t.metrics.retweets)} | Replies ${fmtInt(t.metrics.replies)} | Views ${fmtInt(t.metrics.views)}\n\n`;
md += `[View post](${t.url})\n\n---\n\n`;
}
return md;
}
function toPlainText() {
const tweets = matchedTweets();
const sep = '='.repeat(60);
let txt = `X POSTS EXPORT (${pageLabel()})\n${sep}\n`;
txt += `Scraped: ${new Date().toISOString()}\nMatching posts: ${tweets.length} (of ${store.size} captured)\n\n`;
tweets.forEach((t, i) => {
txt += `[${i + 1}] @${t.author.handle}${t.createdAt ? ' | ' + t.createdAt : ''}\n`;
txt += `${'-'.repeat(60)}\n${t.text}\n\n`;
txt += `Likes: ${fmtInt(t.metrics.likes)} | Reposts: ${fmtInt(t.metrics.retweets)} | Replies: ${fmtInt(t.metrics.replies)} | Views: ${fmtInt(t.metrics.views)}\n`;
txt += `URL: ${t.url}\n${sep}\n\n`;
});
return txt;
}
function toHTML() {
const bundle = exportBundle();
const s = bundle.statistics;
let html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>X posts export (${escapeHtml(bundle.source)})</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; color: #0f1419; }
h1 { color: #1d9bf0; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #ddd; vertical-align: top; }
th { background: #1d9bf0; color: white; position: sticky; top: 0; }
tr:hover { background: #f5f8fa; }
.stats { background: #f5f8fa; padding: 16px 20px; border-radius: 10px; }
.tweet-text { max-width: 480px; white-space: pre-wrap; }
a { color: #1d9bf0; text-decoration: none; }
a:hover { text-decoration: underline; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<h1>X posts export</h1>
<p>Source: ${escapeHtml(bundle.pageUrl)}<br>Scraped: ${bundle.scrapedAt}</p>
<div class="stats">
<strong>Statistics:</strong>
${bundle.totalMatched} posts |
Likes: ${fmtInt(s.totalLikes)} |
Reposts: ${fmtInt(s.totalRetweets)} |
Views: ${fmtInt(s.totalViews)} |
Avg likes: ${fmtInt(s.avgLikes)}
</div>
<table>
<thead><tr><th>#</th><th>Date</th><th>Author</th><th>Post</th><th>Likes</th><th>Reposts</th><th>Replies</th><th>Views</th><th>Link</th></tr></thead>
<tbody>`;
bundle.tweets.forEach((t, i) => {
html += `
<tr>
<td>${i + 1}</td>
<td>${t.createdAt ? t.createdAt.slice(0, 10) : ''}</td>
<td>@${escapeHtml(t.author.handle)}</td>
<td class="tweet-text">${escapeHtml(t.text)}</td>
<td class="num">${fmtInt(t.metrics.likes)}</td>
<td class="num">${fmtInt(t.metrics.retweets)}</td>
<td class="num">${fmtInt(t.metrics.replies)}</td>
<td class="num">${fmtInt(t.metrics.views)}</td>
<td><a href="${escapeHtml(t.url)}" target="_blank" rel="noopener">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 Scraper Toolbox</a> v${VERSION} by @nichxbt
</p>
</body>
</html>`;
return html;
}
const EXPORTERS = {
json: { build: toJSON, ext: 'json', mime: 'application/json' },
csv: { build: toCSV, ext: 'csv', mime: 'text/csv' },
markdown: { build: toMarkdown, ext: 'md', mime: 'text/markdown' },
text: { build: toPlainText, ext: 'txt', mime: 'text/plain' },
html: { build: toHTML, ext: 'html', mime: 'text/html' }
};
function exportAs(format) {
const exporter = EXPORTERS[format];
if (!exporter) { log(`Unknown export format: ${format}`); return; }
const count = matchedTweets().length;
if (count === 0) { log('Nothing to export yet (0 matching posts).'); return; }
const name = `${pageLabel()}_posts_${new Date().toISOString().slice(0, 10)}.${exporter.ext}`;
downloadFile(exporter.build(), name, exporter.mime);
log(`Exported ${count} posts as ${name}`);
}
async function copyAs(kind) {
const count = matchedTweets().length;
if (count === 0) { log('Nothing to copy yet (0 matching posts).'); return; }
const content = kind === 'text' ? toPlainText() : toJSON();
const ok = await copyText(content);
log(ok ? `Copied ${count} posts to clipboard (${kind === 'text' ? 'plain text' : 'JSON'}).`
: 'Clipboard copy failed. Click the page once, then retry.');
}
// ==========================================
// PANEL UI
// ==========================================
const PANEL_ID = 'xactions-toolbox';
let panel, els = {};
const PANEL_HTML = `
<style>
#${PANEL_ID} {
position: fixed;
width: 340px;
max-height: 92vh;
background: #15202b;
color: #e7e9ea;
border: 1px solid #38444d;
border-radius: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
z-index: 2147483647;
box-shadow: 0 8px 30px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
user-select: none;
}
#${PANEL_ID} * { box-sizing: border-box; }
#${PANEL_ID} .xat-header {
background: #1d9bf0; color: #fff;
padding: 10px 14px; cursor: move;
display: flex; align-items: center; justify-content: space-between;
font-weight: 700; font-size: 14px;
border-radius: 15px 15px 0 0;
flex: 0 0 auto;
}
#${PANEL_ID} .xat-header .xat-hbtns { display: flex; gap: 4px; }
#${PANEL_ID} .xat-header button {
background: rgba(255,255,255,0.15); border: none; color: #fff;
width: 24px; height: 24px; border-radius: 6px; cursor: pointer;
font-size: 13px; line-height: 1; transition: background 0.15s;
}
#${PANEL_ID} .xat-header button:hover { background: rgba(255,255,255,0.3); }
#${PANEL_ID} .xat-body { padding: 12px 14px; overflow-y: auto; flex: 1 1 auto; }
#${PANEL_ID} .xat-body::-webkit-scrollbar { width: 8px; }
#${PANEL_ID} .xat-body::-webkit-scrollbar-thumb { background: #38444d; border-radius: 4px; }
#${PANEL_ID} .xat-statusrow { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
#${PANEL_ID} .xat-chip {
padding: 2px 10px; border-radius: 999px; font-weight: 700; font-size: 11px;
background: #38444d; text-transform: uppercase; letter-spacing: 0.5px;
}
#${PANEL_ID} .xat-chip.running { background: #00ba7c; color: #04120c; }
#${PANEL_ID} .xat-chip.paused { background: #ffd400; color: #1a1400; }
#${PANEL_ID} .xat-chip.done { background: #1d9bf0; color: #041018; }
#${PANEL_ID} .xat-elapsed { color: #8899a6; font-variant-numeric: tabular-nums; margin-left: auto; }
#${PANEL_ID} .xat-counts {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 8px 0;
}
#${PANEL_ID} .xat-count {
background: #192734; border-radius: 10px; padding: 8px 6px; text-align: center;
}
#${PANEL_ID} .xat-count b { display: block; font-size: 17px; font-variant-numeric: tabular-nums; }
#${PANEL_ID} .xat-count span { color: #8899a6; font-size: 10px; text-transform: uppercase; letter-spacing: 0.4px; }
#${PANEL_ID} .xat-progress { height: 6px; background: #38444d; border-radius: 3px; overflow: hidden; margin: 4px 0 10px; }
#${PANEL_ID} .xat-progress-bar { height: 100%; width: 0%; background: #1d9bf0; border-radius: 3px; transition: width 0.3s ease; }
#${PANEL_ID} .xat-controls { display: grid; grid-template-columns: 1fr 1fr 1fr 44px; gap: 6px; margin-bottom: 10px; }
#${PANEL_ID} .xat-btn {
border: none; border-radius: 10px; padding: 9px 0; font-weight: 700; font-size: 13px;
cursor: pointer; color: #fff; background: #38444d; transition: filter 0.15s, transform 0.05s;
}
#${PANEL_ID} .xat-btn:hover:not(:disabled) { filter: brightness(1.2); }
#${PANEL_ID} .xat-btn:active:not(:disabled) { transform: scale(0.97); }
#${PANEL_ID} .xat-btn:disabled { opacity: 0.4; cursor: default; }
#${PANEL_ID} .xat-btn.xat-start { background: #00ba7c; color: #04120c; }
#${PANEL_ID} .xat-btn.xat-pause { background: #ffd400; color: #1a1400; }
#${PANEL_ID} .xat-btn.xat-stop { background: #f4212e; }
#${PANEL_ID} details { margin-bottom: 8px; border: 1px solid #38444d; border-radius: 10px; }
#${PANEL_ID} summary {
cursor: pointer; padding: 8px 10px; font-weight: 700; color: #e7e9ea; list-style: none;
display: flex; justify-content: space-between; align-items: center;
}
#${PANEL_ID} summary::after { content: '▾'; color: #8899a6; }
#${PANEL_ID} details[open] summary::after { content: '▴'; }
#${PANEL_ID} .xat-fields { padding: 2px 10px 10px; display: grid; gap: 7px; }
#${PANEL_ID} .xat-field { display: grid; gap: 3px; }
#${PANEL_ID} .xat-field label { color: #8899a6; font-size: 11px; }
#${PANEL_ID} .xat-field input[type="text"], #${PANEL_ID} .xat-field input[type="number"], #${PANEL_ID} .xat-field select {
background: #192734; color: #e7e9ea; border: 1px solid #38444d; border-radius: 8px;
padding: 6px 8px; font-size: 12px; width: 100%; outline: none;
}
#${PANEL_ID} .xat-field input:focus, #${PANEL_ID} .xat-field select:focus { border-color: #1d9bf0; }
#${PANEL_ID} .xat-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
#${PANEL_ID} .xat-checks { display: grid; grid-template-columns: 1fr 1fr; gap: 5px 8px; }
#${PANEL_ID} .xat-checks label {
display: flex; align-items: center; gap: 6px; color: #e7e9ea; font-size: 12px; cursor: pointer;
}
#${PANEL_ID} .xat-checks input { accent-color: #1d9bf0; }
#${PANEL_ID} .xat-exports { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; margin-bottom: 6px; }
#${PANEL_ID} .xat-exports .xat-btn { padding: 7px 0; font-size: 11px; background: #192734; border: 1px solid #38444d; }
#${PANEL_ID} .xat-copyrow { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 8px; }
#${PANEL_ID} .xat-copyrow .xat-btn { background: #192734; border: 1px solid #38444d; font-size: 12px; }
#${PANEL_ID} .xat-preview {
background: #192734; border-radius: 10px; padding: 8px 10px; margin-bottom: 8px;
color: #8899a6; font-size: 11.5px; max-height: 62px; overflow: hidden;
}
#${PANEL_ID} .xat-preview b { color: #e7e9ea; }
#${PANEL_ID} .xat-log {
background: #0d1319; border-radius: 10px; padding: 8px 10px;
font-size: 11px; color: #8899a6; max-height: 76px; overflow-y: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
#${PANEL_ID} .xat-footer {
padding: 6px 14px 10px; color: #536471; font-size: 10.5px; text-align: center; flex: 0 0 auto;
}
#${PANEL_ID} .xat-footer a { color: #1d9bf0; text-decoration: none; }
#${PANEL_ID}.xat-min .xat-body, #${PANEL_ID}.xat-min .xat-footer { display: none; }
</style>
<div class="xat-header" id="xat-drag">
<span>🧰 Scraper Toolbox</span>
<span class="xat-hbtns">
<button id="xat-minimize" title="Minimize" aria-label="Minimize panel">▁</button>
<button id="xat-close" title="Close and restore page (data is kept until reload)" aria-label="Close panel">✕</button>
</span>
</div>
<div class="xat-body">
<div class="xat-statusrow">
<span class="xat-chip" id="xat-status">idle</span>
<span id="xat-page" style="color:#8899a6"></span>
<span class="xat-elapsed" id="xat-elapsed">0s</span>
</div>
<div class="xat-counts">
<div class="xat-count"><b id="xat-captured">0</b><span>captured</span></div>
<div class="xat-count"><b id="xat-matched">0</b><span>matching</span></div>
<div class="xat-count"><b id="xat-scrolls">0</b><span>scrolls</span></div>
</div>
<div class="xat-progress"><div class="xat-progress-bar" id="xat-bar"></div></div>
<div class="xat-controls">
<button class="xat-btn xat-start" id="xat-start">▶ Start</button>
<button class="xat-btn xat-pause" id="xat-pausebtn" disabled>⏸ Pause</button>
<button class="xat-btn xat-stop" id="xat-stopbtn" disabled>⏹ Stop</button>
<button class="xat-btn" id="xat-clear" title="Clear all captured data">🗑</button>
</div>
<details open>
<summary>Scraping</summary>
<div class="xat-fields">
<div class="xat-2col">
<div class="xat-field">
<label for="xat-target">Target posts (0 = no limit)</label>
<input type="number" id="xat-target" min="0" step="10">
</div>
<div class="xat-field">
<label for="xat-delay">Scroll delay (ms)</label>
<input type="number" id="xat-delay" min="400" step="100">
</div>
</div>
<div class="xat-2col">
<div class="xat-field">
<label for="xat-maxscrolls">Max scrolls</label>
<input type="number" id="xat-maxscrolls" min="1" step="10">
</div>
<div class="xat-field">
<label for="xat-stall">Stop after N empty scrolls</label>
<input type="number" id="xat-stall" min="2" step="1">
</div>
</div>
<div class="xat-checks">
<label><input type="checkbox" id="xat-autojson"> Auto-download JSON when done</label>
</div>
</div>
</details>
<details>
<summary>Filters</summary>
<div class="xat-fields">
<div class="xat-field">
<label for="xat-inc">Must contain keywords (comma separated)</label>
<input type="text" id="xat-inc" placeholder="e.g. ai, launch, update">
</div>
<div class="xat-field">
<label for="xat-exc">Exclude keywords</label>
<input type="text" id="xat-exc" placeholder="e.g. giveaway, promo">
</div>
<div class="xat-field">
<label for="xat-onlyusers">Only these users (@handles)</label>
<input type="text" id="xat-onlyusers" placeholder="empty = everyone on this timeline">
</div>
<div class="xat-field">
<label for="xat-skipusers">Skip these users (@handles)</label>
<input type="text" id="xat-skipusers" placeholder="e.g. @spamacct, @bot123">
</div>
<div class="xat-2col">
<div class="xat-field">
<label for="xat-minlikes">Min likes</label>
<input type="number" id="xat-minlikes" min="0">
</div>
<div class="xat-field">
<label for="xat-minrts">Min reposts</label>
<input type="number" id="xat-minrts" min="0">
</div>
</div>
<div class="xat-2col">
<div class="xat-field">
<label for="xat-minviews">Min views</label>
<input type="number" id="xat-minviews" min="0">
</div>
<div class="xat-field">
<label for="xat-days">Last N days (0 = all)</label>
<input type="number" id="xat-days" min="0">
</div>
</div>
<div class="xat-2col">
<div class="xat-field">
<label for="xat-media">Media</label>
<select id="xat-media">
<option value="all">All posts</option>
<option value="with-media">With media only</option>
<option value="without-media">Text only</option>
</select>
</div>
<div class="xat-field">
<label for="xat-lang">Language code (e.g. en)</label>
<input type="text" id="xat-lang" placeholder="empty = all">
</div>
</div>
<div class="xat-checks">
<label><input type="checkbox" id="xat-nort"> No reposts</label>
<label><input type="checkbox" id="xat-noreply"> No replies</label>
<label><input type="checkbox" id="xat-noquote"> No quotes</label>
<label><input type="checkbox" id="xat-nopin"> No pinned</label>
</div>
</div>
</details>
<details open>
<summary>Export</summary>
<div class="xat-fields">
<div class="xat-exports">
<button class="xat-btn" data-export="json">JSON</button>
<button class="xat-btn" data-export="csv">CSV</button>
<button class="xat-btn" data-export="markdown">MD</button>
<button class="xat-btn" data-export="text">TXT</button>
<button class="xat-btn" data-export="html">HTML</button>
</div>
<div class="xat-copyrow">
<button class="xat-btn" id="xat-copyjson">📋 Copy JSON</button>
<button class="xat-btn" id="xat-copytext">📋 Copy clear text</button>
</div>
</div>
</details>
<div class="xat-preview" id="xat-preview">No posts captured yet. Press ▶ Start, or just scroll the page yourself.</div>
<div class="xat-log" id="xat-log"></div>
</div>
<div class="xat-footer">
<a href="https://github.com/nirholas/XActions" target="_blank" rel="noopener">XActions</a> Scraper Toolbox v${VERSION}
</div>`;
function buildPanel() {
const existing = document.getElementById(PANEL_ID);
if (existing) existing.remove();
panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = PANEL_HTML;
document.body.appendChild(panel);
if (config.ui.left !== null) {
panel.style.left = config.ui.left + 'px';
panel.style.top = config.ui.top + 'px';
} else {
panel.style.right = '20px';
panel.style.top = config.ui.top + 'px';
}
const $ = (id) => panel.querySelector('#' + id);
els = {
status: $('xat-status'), page: $('xat-page'), elapsed: $('xat-elapsed'),
captured: $('xat-captured'), matched: $('xat-matched'), scrolls: $('xat-scrolls'),
bar: $('xat-bar'),
start: $('xat-start'), pause: $('xat-pausebtn'), stop: $('xat-stopbtn'), clear: $('xat-clear'),
target: $('xat-target'), delay: $('xat-delay'), maxscrolls: $('xat-maxscrolls'), stall: $('xat-stall'),
autojson: $('xat-autojson'),
inc: $('xat-inc'), exc: $('xat-exc'), onlyusers: $('xat-onlyusers'), skipusers: $('xat-skipusers'),
minlikes: $('xat-minlikes'), minrts: $('xat-minrts'), minviews: $('xat-minviews'), days: $('xat-days'),
media: $('xat-media'), lang: $('xat-lang'),
nort: $('xat-nort'), noreply: $('xat-noreply'), noquote: $('xat-noquote'), nopin: $('xat-nopin'),
preview: $('xat-preview'), log: $('xat-log')
};
els.page.textContent = '/' + pageLabel();
applyConfigToUI();
// Controls
els.start.addEventListener('click', start);
els.pause.addEventListener('click', togglePause);
els.stop.addEventListener('click', stop);
els.clear.addEventListener('click', clearData);
$('xat-minimize').addEventListener('click', () => panel.classList.toggle('xat-min'));
$('xat-close').addEventListener('click', destroy);
$('xat-copyjson').addEventListener('click', () => copyAs('json'));
$('xat-copytext').addEventListener('click', () => copyAs('text'));
panel.querySelectorAll('[data-export]').forEach((btn) =>
btn.addEventListener('click', () => exportAs(btn.dataset.export)));
// Any input change re-reads config, persists it, and refreshes counters
panel.querySelectorAll('input, select').forEach((el) =>
el.addEventListener('change', () => { readConfigFromUI(); saveConfig(); updateStats(); }));
// Drag
const header = $('xat-drag');
let drag = null;
header.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
const rect = panel.getBoundingClientRect();
drag = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
e.preventDefault();
});
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
function onDragMove(e) {
if (!drag) return;
const left = Math.max(0, Math.min(window.innerWidth - 60, e.clientX - drag.dx));
const top = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - drag.dy));
panel.style.left = left + 'px';
panel.style.top = top + 'px';
panel.style.right = 'auto';
}
function onDragEnd() {
if (!drag) return;
drag = null;
const rect = panel.getBoundingClientRect();
config.ui.left = Math.round(rect.left);
config.ui.top = Math.round(rect.top);
saveConfig();
}
panel.__dragCleanup = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
};
}
function applyConfigToUI() {
const f = config.filters;
els.target.value = config.targetCount;
els.delay.value = config.scrollDelay;
els.maxscrolls.value = config.maxScrolls;
els.stall.value = config.stallLimit;
els.autojson.checked = config.autoDownloadJson;
els.inc.value = f.includeKeywords.join(', ');
els.exc.value = f.excludeKeywords.join(', ');
els.onlyusers.value = f.onlyUsers.map((u) => '@' + u).join(', ');
els.skipusers.value = f.excludeUsers.map((u) => '@' + u).join(', ');
els.minlikes.value = f.minLikes;
els.minrts.value = f.minRetweets;
els.minviews.value = f.minViews;
els.days.value = f.daysBack;
els.media.value = f.mediaFilter;
els.lang.value = f.lang;
els.nort.checked = f.excludeRetweets;
els.noreply.checked = f.excludeReplies;
els.noquote.checked = f.excludeQuotes;
els.nopin.checked = f.excludePinned;
}
function readConfigFromUI() {
const num = (el, min, fallback) => {
const v = parseInt(el.value, 10);
return Number.isFinite(v) && v >= min ? v : fallback;
};
config.targetCount = num(els.target, 0, DEFAULT_CONFIG.targetCount);
config.scrollDelay = num(els.delay, 400, DEFAULT_CONFIG.scrollDelay);
config.maxScrolls = num(els.maxscrolls, 1, DEFAULT_CONFIG.maxScrolls);
config.stallLimit = num(els.stall, 2, DEFAULT_CONFIG.stallLimit);
config.autoDownloadJson = els.autojson.checked;
const f = config.filters;
f.includeKeywords = csvList(els.inc.value);
f.excludeKeywords = csvList(els.exc.value);
f.onlyUsers = csvList(els.onlyusers.value);
f.excludeUsers = csvList(els.skipusers.value);
f.minLikes = num(els.minlikes, 0, 0);
f.minRetweets = num(els.minrts, 0, 0);
f.minViews = num(els.minviews, 0, 0);
f.daysBack = num(els.days, 0, 0);
f.mediaFilter = els.media.value;
f.lang = els.lang.value.trim().toLowerCase();
f.excludeRetweets = els.nort.checked;
f.excludeReplies = els.noreply.checked;
f.excludeQuotes = els.noquote.checked;
f.excludePinned = els.nopin.checked;
}
// ==========================================
// UI UPDATES
// ==========================================
function setStatus(status) {
state.status = status;
if (!els.status) return;
els.status.textContent = status;
els.status.className = 'xat-chip ' + status;
const running = status === 'running';
const paused = status === 'paused';
els.start.disabled = running || paused;
els.pause.disabled = !running && !paused;
els.pause.textContent = paused ? '▶ Resume' : '⏸ Pause';
els.stop.disabled = !running && !paused;
}
function updateStats() {
if (!els.captured) return;
const matched = matchedTweets().length;
els.captured.textContent = fmtInt(store.size);
els.matched.textContent = fmtInt(matched);
els.scrolls.textContent = fmtInt(state.scrolls);
const target = config.targetCount;
els.bar.style.width = target > 0 ? Math.min(100, (matched / target) * 100) + '%' : (store.size > 0 ? '100%' : '0%');
}
function previewLatest() {
if (!els.preview) return;
let t = null;
for (const v of store.values()) t = v; // last inserted record
if (!t) return;
const snippet = (t.text || '(media only)').replace(/\s+/g, ' ').slice(0, 120);
els.preview.innerHTML = `<b>@${escapeHtml(t.author.handle)}</b> · ${escapeHtml(snippet)}`;
}
function log(msg) {
console.log(`${TAG} ${msg}`);
if (!els.log) return;
const line = document.createElement('div');
line.textContent = `${new Date().toTimeString().slice(0, 8)} ${msg}`;
els.log.prepend(line);
while (els.log.children.length > 40) els.log.lastChild.remove();
}
// ==========================================
// SCROLL ENGINE
// ==========================================
let runToken = 0; // invalidates a running loop after stop/clear/destroy
async function start() {
if (state.status === 'running' || state.status === 'paused') return;
readConfigFromUI();
saveConfig();
state.scrolls = 0;
state.stalls = 0;
state.startedAt = Date.now();
state.elapsedBefore = 0;
state.lastChangeCount = store.size;
setStatus('running');
log(`Run started on /${pageLabel()} (target: ${config.targetCount || 'unlimited'}).`);
const token = ++runToken;
domSweep(); // catch what's already rendered
while (token === runToken && !state.destroyed) {
if (state.status === 'paused') { await sleep(250); continue; }
if (state.status !== 'running') break;
const matched = matchedTweets().length;
if (config.targetCount > 0 && matched >= config.targetCount) {
finishRun(`Target reached: ${matched} matching posts.`);
break;
}
if (state.scrolls >= config.maxScrolls) {
finishRun(`Max scrolls (${config.maxScrolls}) reached with ${matched} matching posts.`);
break;
}
// Scroll with light jitter so loading keeps up
const el = document.scrollingElement || document.documentElement;
el.scrollTop = el.scrollHeight;
state.scrolls++;
await sleep(config.scrollDelay + Math.floor(Math.random() * 400));
if (token !== runToken || state.destroyed) break;
domSweep();
if (store.size === state.lastChangeCount) {
state.stalls++;
if (state.stalls >= config.stallLimit) {
finishRun(`End of timeline: nothing new after ${config.stallLimit} scrolls (${matchedTweets().length} matching posts).`);
break;
}
} else {
state.stalls = 0;
state.lastChangeCount = store.size;
}
updateStats();
}
}
function togglePause() {
if (state.status === 'running') {
state.elapsedBefore += Date.now() - state.startedAt;
setStatus('paused');
log('Paused. Capture stays live; scrolling is halted.');
} else if (state.status === 'paused') {
state.startedAt = Date.now();
setStatus('running');
log('Resumed.');
}
}
function stop() {
if (state.status !== 'running' && state.status !== 'paused') return;
runToken++;
finishRun(`Stopped manually with ${matchedTweets().length} matching posts.`);
}
function finishRun(message) {
if (state.status === 'paused') state.startedAt = Date.now();
state.elapsedBefore += Date.now() - state.startedAt;
setStatus('done');
updateStats();
log(message);
log('Adjust filters freely, then export: results always reflect current filters.');
if (config.autoDownloadJson && matchedTweets().length > 0) exportAs('json');
}
function clearData() {
runToken++;
store.clear();
state.scrolls = 0;
state.stalls = 0;
state.elapsedBefore = 0;
state.startedAt = null;
setStatus('idle');
updateStats();
if (els.preview) els.preview.textContent = 'Cleared. Press ▶ Start to scrape again.';
log('All captured data cleared.');
}
// ==========================================
// TICKER
// ==========================================
const ticker = setInterval(() => {
if (state.destroyed || !els.elapsed) return;
let ms = state.elapsedBefore;
if (state.status === 'running' && state.startedAt) ms += Date.now() - state.startedAt;
els.elapsed.textContent = fmtElapsed(ms);
}, 500);
// ==========================================
// LIFECYCLE
// ==========================================
function destroy() {
state.destroyed = true;
runToken++;
clearInterval(ticker);
removeInterceptor();
if (panel) {
if (panel.__dragCleanup) panel.__dragCleanup();
panel.remove();
panel = null;
}
delete window.XActionsToolbox;
console.log(`${TAG} Closed. fetch/XHR restored. Paste the script again to reopen.`);
}
// ==========================================
// BOOT
// ==========================================
if (!/(^|\.)x\.com$|(^|\.)twitter\.com$/.test(location.hostname)) {
console.warn(`${TAG} This script must run on x.com. Open a profile or timeline there and paste again.`);
return;
}
installInterceptor();
buildPanel();
setStatus('idle');
updateStats();
log(`Toolbox v${VERSION} ready on /${pageLabel()}. Capture is live even before you press Start.`);
window.XActionsToolbox = {
version: VERSION,
tweets: allTweets,
matched: matchedTweets,
export: exportAs,
copy: copyAs,
start,
pause: togglePause,
stop,
clear: clearData,
destroy,
config
};
})();
});
register("send-direct-message", function(){
var CONFIG = {
// Target users to message
targetUsers: [
// 'username1',
// 'username2',
],
// Message template
// Use {username} as placeholder for recipient's name
messageTemplate: `Hey {username}! 👋
Just wanted to reach out and connect.
Best,
[Your Name]`,
// Limits
limits: {
messagesPerSession: 10,
delayBetweenMessages: 30000, // 30 seconds (be careful!)
},
// Options
options: {
skipIfConversationExists: true, // Don't message if already in conversation
randomizeDelay: true, // Add randomness to delays
},
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function sendDirectMessage() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Set an input's value through the native setter so React's value tracker
// registers the change (direct .value assignment gets ignored by React).
const setNativeValue = (el, value) => {
const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value');
if (desc && desc.set) desc.set.call(el, value);
else el.value = value;
};
const randomDelay = (base) => {
if (CONFIG.options.randomizeDelay) {
return sleep(base + Math.random() * base * 0.5);
}
return sleep(base);
};
// DOM Selectors
const SELECTORS = {
newMessageBtn: '[data-testid="NewDM_Button"]',
searchInput: '[data-testid="searchPeople"]',
userCell: '[data-testid="UserCell"]',
messageInput: '[data-testid="dmComposerTextInput"]',
sendButton: '[data-testid="dmComposerSendButton"]',
conversationList: '[data-testid="conversation"]',
backButton: '[data-testid="app-bar-back"]',
};
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 💬 SEND DIRECT MESSAGES ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
console.log('⚠️ WARNING: Use responsibly! Mass DMing can get you restricted.');
console.log('');
// State
const state = {
isRunning: false,
stats: { sent: 0, failed: 0, skipped: 0 },
messageLog: [],
};
// Storage for tracking sent messages
const storage = {
get: (key) => {
try { return JSON.parse(localStorage.getItem(`xactions_dm_${key}`) || 'null'); }
catch { return null; }
},
set: (key, value) => {
localStorage.setItem(`xactions_dm_${key}`, JSON.stringify(value));
}
};
// Get sent messages history
const getSentHistory = () => storage.get('sent') || [];
const markAsSent = (username) => {
const history = getSentHistory();
if (!history.includes(username)) {
history.push(username);
storage.set('sent', history);
}
};
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.DM = {
config: CONFIG,
state,
// Send DM to a single user
sendTo: async (username, customMessage = null) => {
const cleanUsername = username.replace('@', '').toLowerCase();
// split/join instead of .replace() so every occurrence of the
// placeholder is substituted, not just the first one.
const message = customMessage || CONFIG.messageTemplate.split('{username}').join(cleanUsername);
console.log(`💬 Sending DM to @${cleanUsername}...`);
// Check if already messaged
if (getSentHistory().includes(cleanUsername)) {
console.log(`⏭️ Already messaged @${cleanUsername}, skipping.`);
state.stats.skipped++;
return false;
}
try {
// Click new message button
const newMsgBtn = document.querySelector(SELECTORS.newMessageBtn);
if (newMsgBtn) {
newMsgBtn.click();
await sleep(1500);
}
// Search for user
const searchInput = document.querySelector(SELECTORS.searchInput);
if (!searchInput) {
console.error('❌ Search input not found. Are you on Messages page?');
return false;
}
// Type username
searchInput.focus();
setNativeValue(searchInput, cleanUsername);
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(2000);
// Click on user - match the exact @handle so a prefix like "john"
// can't select "johnny" (wrong DM recipient)
const userCells = document.querySelectorAll(SELECTORS.userCell);
const handleRe = new RegExp('@' + cleanUsername.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?![a-zA-Z0-9_])', 'i');
let found = false;
for (const cell of userCells) {
if (handleRe.test(cell.textContent)) {
cell.click();
found = true;
await sleep(1500);
break;
}
}
if (!found) {
console.error(`❌ User @${cleanUsername} not found.`);
state.stats.failed++;
return false;
}
// Click Next if present
const nextBtn = document.querySelector('[data-testid="nextButton"]');
if (nextBtn) {
nextBtn.click();
await sleep(1500);
}
// Type message
const msgInput = document.querySelector(SELECTORS.messageInput);
if (!msgInput) {
console.error('❌ Message input not found.');
state.stats.failed++;
return false;
}
msgInput.focus();
// Use execCommand for better compatibility
document.execCommand('insertText', false, message);
await sleep(1000);
// Send message
const sendBtn = document.querySelector(SELECTORS.sendButton);
if (sendBtn && !sendBtn.disabled) {
sendBtn.click();
await sleep(1500);
markAsSent(cleanUsername);
state.stats.sent++;
state.messageLog.push({
to: cleanUsername,
message,
timestamp: new Date().toISOString(),
});
console.log(`✅ Message sent to @${cleanUsername}!`);
return true;
} else {
console.error('❌ Send button not available.');
state.stats.failed++;
return false;
}
} catch (e) {
console.error(`❌ Error sending to @${cleanUsername}:`, e.message);
state.stats.failed++;
return false;
}
},
// Send to all target users
sendToAll: async () => {
if (CONFIG.targetUsers.length === 0) {
console.error('❌ No target users configured!');
return;
}
console.log(`🚀 Starting to send DMs to ${CONFIG.targetUsers.length} users...`);
console.log(`⏱️ Delay between messages: ${CONFIG.limits.delayBetweenMessages / 1000}s`);
console.log('');
state.isRunning = true;
for (let i = 0; i < CONFIG.targetUsers.length; i++) {
if (!state.isRunning) break;
if (state.stats.sent >= CONFIG.limits.messagesPerSession) {
console.log(`🛑 Reached limit of ${CONFIG.limits.messagesPerSession} messages.`);
break;
}
await window.XActions.DM.sendTo(CONFIG.targetUsers[i]);
// Use the loop index, not indexOf(), so a duplicate username in
// targetUsers can't make this resolve to the wrong (earlier) entry
// and skip the delay before the real last message.
if (state.isRunning && i < CONFIG.targetUsers.length - 1) {
console.log(`⏳ Waiting ${CONFIG.limits.delayBetweenMessages / 1000}s before next message...`);
await randomDelay(CONFIG.limits.delayBetweenMessages);
}
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 DM SESSION COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
window.XActions.DM.stats();
},
// Add user to target list
addUser: (username) => {
const clean = username.replace('@', '').toLowerCase();
if (!CONFIG.targetUsers.includes(clean)) {
CONFIG.targetUsers.push(clean);
console.log(`✅ Added @${clean} to DM list`);
}
},
// Set message template
setMessage: (template) => {
CONFIG.messageTemplate = template;
console.log('✅ Message template updated.');
console.log('Preview:', template.replace('{username}', 'example'));
},
// Stop
stop: () => {
state.isRunning = false;
console.log('🛑 Stopped.');
},
// Stats
stats: () => {
console.log('');
console.log('📊 DM STATS:');
console.log(` ✅ Sent: ${state.stats.sent}`);
console.log(` ❌ Failed: ${state.stats.failed}`);
console.log(` ⏭️ Skipped: ${state.stats.skipped}`);
console.log(` 📜 Total messaged (all time): ${getSentHistory().length}`);
console.log('');
},
// View message log
log: () => {
console.log('');
console.log('📜 MESSAGE LOG:');
state.messageLog.forEach(m => {
console.log(` → @${m.to} at ${m.timestamp}`);
});
console.log('');
},
// Clear sent history
clearHistory: () => {
if (confirm('⚠️ Clear all sent message history?')) {
storage.set('sent', []);
console.log('✅ History cleared.');
}
},
// Help
help: () => {
console.log('');
console.log('📋 DM COMMANDS:');
console.log('');
console.log(' XActions.DM.addUser("username")');
console.log(' XActions.DM.setMessage("Your message {username}")');
console.log(' XActions.DM.sendTo("username")');
console.log(' XActions.DM.sendToAll()');
console.log(' XActions.DM.stop()');
console.log(' XActions.DM.stats()');
console.log(' XActions.DM.log()');
console.log(' XActions.DM.clearHistory()');
console.log('');
console.log('💡 Use {username} in message template as placeholder.');
console.log('⚠️ Always be respectful and don\'t spam!');
console.log('');
}
};
console.log('✅ Direct Message Helper loaded!');
console.log(' Run XActions.DM.help() for commands.');
console.log('');
})();
});
register("sentiment-analyzer", function(){
(async function sentimentAnalyzer() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of posts to score.
maxPosts: 40,
// Max scroll attempts before giving up on loading more posts.
maxScrollAttempts: 25,
// Delay between scrolls (ms).
scrollDelay: 1600,
// How many example posts to show for most-positive / most-negative.
examples: 3,
// Auto-download a JSON report when finished.
exportResults: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
authorLink: 'a[href^="/"][role="link"]'
};
// ============================================
// 📚 SENTIMENT LEXICON (inline, no external API)
// ============================================
const POSITIVE = {
amazing: 3, incredible: 3, fantastic: 3, brilliant: 3, outstanding: 3,
excellent: 3, wonderful: 3, phenomenal: 3, revolutionary: 3, legendary: 3,
great: 2, awesome: 2, love: 2, perfect: 2, beautiful: 2, impressive: 2,
thrilled: 2, excited: 2, grateful: 2, blessed: 2, proud: 2, inspired: 2,
superb: 2, remarkable: 2, winning: 2, victory: 2, bullish: 2,
good: 1, nice: 1, happy: 1, like: 1, thanks: 1, helpful: 1,
interesting: 1, cool: 1, agree: 1, support: 1, hope: 1, enjoy: 1,
fun: 1, smart: 1, useful: 1, solid: 1, better: 1, congrats: 1
};
const NEGATIVE = {
terrible: 3, horrible: 3, disgusting: 3, pathetic: 3, disastrous: 3,
hate: 3, despise: 3, devastating: 3, atrocious: 3, unforgivable: 3,
bad: 2, awful: 2, worst: 2, angry: 2, furious: 2, disappointed: 2,
frustrated: 2, annoying: 2, stupid: 2, trash: 2, garbage: 2, broken: 2,
scam: 2, toxic: 2, ridiculous: 2, ruined: 2, useless: 2, failed: 2, bearish: 2,
sad: 1, boring: 1, wrong: 1, slow: 1, confusing: 1, worried: 1,
problem: 1, issue: 1, lost: 1, doubt: 1, fear: 1, weak: 1, worse: 1
};
const NEGATORS = new Set(['not', "n't", 'no', 'never', 'hardly', 'barely']);
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const download = (data, filename) => {
try {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
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);
log.success(`Report downloaded: ${filename}`);
} catch (e) {
log.error(`Could not download report: ${e.message}`);
}
};
// Score one post. Handles simple negation ("not good" flips the sign).
const analyze = (text) => {
const words = text.toLowerCase().replace(/[^\w\s']/g, ' ').split(/\s+/).filter(Boolean);
let score = 0;
const posWords = [];
const negWords = [];
for (let i = 0; i < words.length; i++) {
const word = words[i];
const prev = i > 0 ? words[i - 1] : '';
const negated = NEGATORS.has(prev) || prev.endsWith("n't");
if (POSITIVE[word] !== undefined) {
score += POSITIVE[word] * (negated ? -0.5 : 1);
if (!negated) posWords.push(word);
}
if (NEGATIVE[word] !== undefined) {
score -= NEGATIVE[word] * (negated ? -0.5 : 1);
if (!negated) negWords.push(word);
}
}
const normalized = Math.max(-1, Math.min(1, score / Math.max(words.length * 0.3, 1)));
const rounded = Math.round(normalized * 100) / 100;
const label = rounded > 0.15 ? 'positive' : rounded < -0.15 ? 'negative' : 'neutral';
return { score: rounded, label, positiveWords: [...new Set(posWords)], negativeWords: [...new Set(negWords)] };
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🧠 SENTIMENT ANALYZER - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
if (!/(^|\.)x\.com$/.test(location.hostname) && !/(^|\.)twitter\.com$/.test(location.hostname)) {
log.warning('Not on x.com. Open a timeline, search, or profile and run again.');
return;
}
log.info(`Scoring up to ${CONFIG.maxPosts} posts on this page`);
log.warning('Lexicon-based scoring. It cannot detect sarcasm, slang, or context.');
log.info('To stop early: window.stopSentimentAnalyzer()');
console.log('');
let stopped = false;
window.stopSentimentAnalyzer = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then reporting.');
};
const collected = new Map();
let scrollAttempts = 0;
let stalls = 0;
const collect = () => {
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
const textEl = article.querySelector(SELECTORS.tweetText);
if (!textEl) return;
const text = textEl.textContent.trim();
if (text.length < 5 || collected.has(text)) return;
const authorLink = article.querySelector(SELECTORS.authorLink);
const author = authorLink
? (authorLink.getAttribute('href') || '').replace(/^\//, '').split('/')[0]
: 'unknown';
collected.set(text, { text, author });
});
};
while (!stopped && collected.size < CONFIG.maxPosts && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = collected.size;
collect();
if (collected.size > before) {
log.info(`Collected ${collected.size}/${CONFIG.maxPosts} posts`);
stalls = 0;
} else {
stalls++;
if (stalls >= 5) {
log.warning('No new posts after several scrolls. Reached the end of what loaded.');
break;
}
}
if (collected.size >= CONFIG.maxPosts) break;
window.scrollTo(0, document.body.scrollHeight);
scrollAttempts++;
await sleep(CONFIG.scrollDelay);
}
collect();
if (collected.size === 0) {
log.error('No posts found on this page. Scroll to some posts and run again.');
return;
}
const results = [];
let positive = 0;
let negative = 0;
let neutral = 0;
let totalScore = 0;
const wordFreq = {};
for (const tweet of collected.values()) {
if (results.length >= CONFIG.maxPosts) break;
const sentiment = analyze(tweet.text);
results.push({ ...tweet, ...sentiment });
totalScore += sentiment.score;
if (sentiment.label === 'positive') positive++;
else if (sentiment.label === 'negative') negative++;
else neutral++;
[...sentiment.positiveWords, ...sentiment.negativeWords].forEach((w) => {
wordFreq[w] = (wordFreq[w] || 0) + 1;
});
}
const total = results.length;
const avgScore = Math.round((totalScore / total) * 1000) / 1000;
const mood = avgScore > 0.1 ? '😊 Positive' : avgScore < -0.1 ? '😠 Negative' : '😐 Neutral';
// ============================================
// 📊 SUMMARY
// ============================================
const barW = 40;
const posBar = Math.round((positive / total) * barW);
const negBar = Math.round((negative / total) * barW);
const neuBar = Math.max(0, barW - posBar - negBar);
const pct = (n) => ((n / total) * 100).toFixed(1) + '%';
console.log('');
console.log('━'.repeat(60));
console.log(`🧠 SENTIMENT REPORT (${total} posts) at ${window.location.pathname}`);
console.log('━'.repeat(60));
console.log(` Average score: ${avgScore} (${mood})`);
console.log('');
console.log(` 😊 Positive: ${String(positive).padStart(3)} (${pct(positive).padStart(6)}) ${'█'.repeat(posBar)}`);
console.log(` 😐 Neutral: ${String(neutral).padStart(3)} (${pct(neutral).padStart(6)}) ${'░'.repeat(neuBar)}`);
console.log(` 😠 Negative: ${String(negative).padStart(3)} (${pct(negative).padStart(6)}) ${'▓'.repeat(negBar)}`);
const sorted = [...results].sort((a, b) => b.score - a.score);
const topPos = sorted.filter((t) => t.label === 'positive').slice(0, CONFIG.examples);
const topNeg = sorted.filter((t) => t.label === 'negative').slice(-CONFIG.examples).reverse();
if (topPos.length > 0) {
console.log('');
console.log('🏆 Most positive:');
topPos.forEach((t) => console.log(` [${t.score.toFixed(2)}] @${t.author}: "${t.text.replace(/\s+/g, ' ').slice(0, 100)}"`));
}
if (topNeg.length > 0) {
console.log('');
console.log('💀 Most negative:');
topNeg.forEach((t) => console.log(` [${t.score.toFixed(2)}] @${t.author}: "${t.text.replace(/\s+/g, ' ').slice(0, 100)}"`));
}
const topWords = Object.entries(wordFreq).sort((a, b) => b[1] - a[1]).slice(0, 10);
if (topWords.length > 0) {
console.log('');
console.log('📝 Top emotional words:');
topWords.forEach(([word, count]) => {
console.log(` ${POSITIVE[word] !== undefined ? '😊' : '😠'} "${word}" x${count}`);
});
}
const stronglyNegative = results.filter((t) => t.score < -0.5).length;
if (stronglyNegative > total * 0.2) {
console.log('');
log.warning(`High negativity: ${((stronglyNegative / total) * 100).toFixed(0)}% of posts are strongly negative.`);
}
const report = {
page: window.location.href,
analyzedAt: new Date().toISOString(),
summary: { total, positive, negative, neutral, avgScore, stronglyNegative },
topWords,
posts: results
};
if (CONFIG.exportResults) {
console.log('');
download(report, `xactions-sentiment-${new Date().toISOString().slice(0, 10)}.json`);
}
window.xactionsSentiment = report;
console.log('');
log.info('Full report object: window.xactionsSentiment');
log.success('Done.');
return report;
})();
});
register("shadowban-checker", function(){
(async function shadowbanChecker() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Handle to check. 'auto' detects the logged-in account from the page.
// Set to a bare handle (no @) to check any public account.
username: 'auto',
// Delay between the individual checks (ms). Keep this polite so X does
// not rate-limit the read requests.
testDelay: 2500,
// Auto-download a JSON report of the results when finished.
exportResults: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
unknown: (msg) => console.log(`❓ ${msg}`)
};
const download = (data, filename) => {
try {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
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);
log.success(`Report downloaded: ${filename}`);
} catch (e) {
log.error(`Could not download report: ${e.message}`);
}
};
// Fetch a page's HTML in the current session. Used for search-visibility
// heuristics: we cannot see X's internal ranking, only whether the handle
// shows up in the returned markup.
const fetchPage = async (url) => {
try {
const resp = await fetch(url, {
credentials: 'include',
headers: { Accept: 'text/html,application/xhtml+xml' }
});
if (!resp.ok) return { status: resp.status, text: '' };
return { status: resp.status, text: await resp.text() };
} catch (e) {
return { status: 0, text: '', error: e.message };
}
};
// Detect the logged-in handle from the nav profile link or a meta tag.
const detectUsername = () => {
const navLink = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
const href = navLink?.getAttribute('href') || '';
const navMatch = href.match(/^\/([A-Za-z0-9_]+)/);
if (navMatch) return navMatch[1];
const meta = document.querySelector('meta[property="al:android:url"]');
const metaMatch = meta?.getAttribute('content')?.match(/screen_name=([^&]+)/);
if (metaMatch) return metaMatch[1];
return null;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🕵️ SHADOWBAN CHECKER - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: any x.com / twitter.com page works because we only issue
// read requests, but we do need to be on the site for session cookies.
if (!/(^|\.)x\.com$/.test(location.hostname) && !/(^|\.)twitter\.com$/.test(location.hostname)) {
log.warning('Not on x.com. Open x.com (any page, logged in) and run this again.');
return;
}
const username = CONFIG.username === 'auto' ? detectUsername() : CONFIG.username.replace(/^@/, '').trim();
if (!username) {
log.error('Could not detect your username. Set CONFIG.username manually (without the @).');
return;
}
log.info(`Checking @${username} for shadowban indicators`);
log.warning('These are HEURISTICS, not an official ruling. X does not expose real');
log.warning('shadowban status. Treat the result as a signal, not proof.');
console.log('');
const results = {
username,
timestamp: new Date().toISOString(),
disclaimer: 'Heuristic checks run from the browser. Not an official or definitive shadowban verdict.',
tests: {},
overallStatus: 'UNKNOWN'
};
// ── Test 1: Account existence ────────────────
log.info('Test 1: Account existence');
const profileResp = await fetchPage(`https://x.com/${username}`);
if (profileResp.status === 404 || /Account suspended/i.test(profileResp.text)) {
results.tests.exists = { status: 'FAIL', detail: 'Account not found or suspended' };
results.overallStatus = 'SUSPENDED / NOT FOUND';
log.error('Account not found or suspended. Stopping.');
if (CONFIG.exportResults) download(results, `xactions-shadowban-${username}-${new Date().toISOString().slice(0, 10)}.json`);
return;
}
results.tests.exists = { status: 'PASS', detail: 'Account exists and is active' };
log.success('Account exists and is active');
await sleep(CONFIG.testDelay);
// ── Test 2: Search suggestion ban ────────────
// Heuristic: query the typeahead for the handle and see whether it comes
// back. A missing handle can mean a suggestion ban, but can also just be a
// low-signal or brand-new account.
console.log('');
log.info('Test 2: Search suggestion ban (typeahead)');
try {
const resp = await fetch(
`https://x.com/i/api/1.1/search/typeahead.json?q=${encodeURIComponent(username)}&src=search_box&result_type=users`,
{ credentials: 'include', headers: { 'x-twitter-active-user': 'yes' } }
);
if (resp.ok) {
const data = await resp.json();
const found = Array.isArray(data.users) && data.users.some(
(u) => u.screen_name?.toLowerCase() === username.toLowerCase()
);
results.tests.suggestion = {
status: found ? 'PASS' : 'WARN',
detail: found ? 'Appears in search suggestions' : 'Not appearing in suggestions (possible suggestion ban)'
};
if (found) log.success('Appears in search suggestions');
else log.warning('Not appearing in suggestions. Possible suggestion ban (or just low signal).');
} else {
results.tests.suggestion = { status: 'UNKNOWN', detail: `Typeahead API returned ${resp.status}` };
log.unknown(`Could not check (typeahead API ${resp.status})`);
}
} catch (e) {
results.tests.suggestion = { status: 'UNKNOWN', detail: e.message };
log.unknown(`Could not check: ${e.message}`);
}
await sleep(CONFIG.testDelay);
// ── Test 3: Search ban ───────────────────────
// Heuristic: request the live search for from:<handle> and check whether
// the handle shows up in the returned HTML. The rendered search page is
// JS-driven, so this is a coarse signal only.
console.log('');
log.info('Test 3: Search ban (tweet visibility in from: search)');
try {
const resp = await fetchPage(`https://x.com/search?q=${encodeURIComponent('from:' + username)}&f=live`);
const found = new RegExp(`[/@]${username}\\b`, 'i').test(resp.text) || resp.text.toLowerCase().includes(username.toLowerCase());
results.tests.searchBan = {
status: found ? 'PASS' : 'WARN',
detail: found ? 'Handle appears in its own from: search markup' : 'Handle may not appear in search (possible search ban)'
};
if (found) log.success('Handle appears in its own from: search');
else log.warning('Handle may not appear in search. Possible search ban.');
} catch (e) {
results.tests.searchBan = { status: 'UNKNOWN', detail: e.message };
log.unknown(`Could not check: ${e.message}`);
}
await sleep(CONFIG.testDelay);
// ── Test 4: Reply deboost hint ───────────────
// Heuristic: check whether the account's replies show up in a replies-only
// search. A miss hints at reply deboosting but is far from conclusive.
console.log('');
log.info('Test 4: Reply deboost hint (replies in search)');
try {
const resp = await fetchPage(`https://x.com/search?q=${encodeURIComponent('from:' + username + ' filter:replies')}&f=live`);
const found = resp.text.toLowerCase().includes(username.toLowerCase());
results.tests.replyDeboost = {
status: found ? 'PASS' : 'WARN',
detail: found ? 'Replies appear visible in search' : 'Replies may be hidden (possible reply deboost)'
};
if (found) log.success('Replies appear visible in search');
else log.warning('Replies may be hidden. Possible reply deboost.');
} catch (e) {
results.tests.replyDeboost = { status: 'UNKNOWN', detail: e.message };
log.unknown(`Could not check: ${e.message}`);
}
// ── Overall assessment ───────────────────────
const testList = Object.values(results.tests);
const fails = testList.filter((t) => t.status === 'FAIL').length;
const warns = testList.filter((t) => t.status === 'WARN').length;
if (fails > 0) results.overallStatus = 'LIKELY SHADOWBANNED';
else if (warns >= 2) results.overallStatus = 'POSSIBLY SHADOWBANNED';
else if (warns === 1) results.overallStatus = 'MINOR SIGNALS';
else results.overallStatus = 'CLEAN (no signals detected)';
// ============================================
// 📊 SUMMARY
// ============================================
console.log('');
console.log('━'.repeat(60));
console.log(`📊 SHADOWBAN HEURISTIC REPORT: @${username}`);
console.log('━'.repeat(60));
for (const [name, result] of Object.entries(results.tests)) {
const icon = result.status === 'PASS' ? '✅' : result.status === 'FAIL' ? '❌' : result.status === 'WARN' ? '⚠️' : '❓';
console.log(` ${icon} ${name.padEnd(14)} ${result.detail}`);
}
const statusIcon = results.overallStatus.startsWith('CLEAN') ? '✅'
: results.overallStatus.includes('LIKELY') ? '❌' : '⚠️';
console.log('');
console.log(` ${statusIcon} Overall: ${results.overallStatus}`);
console.log('');
console.log(' ℹ️ Reminder: these are browser-side heuristics. A WARN can be caused');
console.log(' by a new/low-activity account, a private profile, or a rate limit,');
console.log(' not only by a real shadowban.');
if (!results.overallStatus.startsWith('CLEAN')) {
console.log('');
console.log('💡 If you see repeated signals:');
console.log(' 1. Pause automated activity for 48-72 hours.');
console.log(' 2. Remove content that may violate the rules.');
console.log(' 3. Verify your email and phone number.');
console.log(' 4. Engage naturally: like, reply, browse.');
}
// Save a rolling history in localStorage for later comparison.
try {
const key = `xactions_shadowban_${username}`;
const history = JSON.parse(localStorage.getItem(key) || '[]');
history.push(results);
localStorage.setItem(key, JSON.stringify(history.slice(-30)));
console.log('');
log.info(`Snapshot saved. Retrieve: JSON.parse(localStorage.getItem("${key}"))`);
} catch (e) {
log.warning(`Could not save snapshot: ${e.message}`);
}
if (CONFIG.exportResults) {
console.log('');
download(results, `xactions-shadowban-${username}-${new Date().toISOString().slice(0, 10)}.json`);
}
window.xactionsShadowban = results;
console.log('');
log.info('Full result object: window.xactionsShadowban');
return results;
})();
});
register("smart-unfollow", function(){
var CONFIG = {
// ---- TIMING ----
// Days to wait before unfollowing non-followers
// 💡 Give users time to follow back!
daysToWait: 3,
// ---- LIMITS ----
// Maximum unfollows per session
maxUnfollows: 30,
// ---- PROTECTION ----
// Usernames to NEVER unfollow
// 💡 Add important accounts here
whitelist: [
// 'elonmusk',
// 'naval',
],
// Only unfollow users tracked by keyword-follow
// 💡 Set to false to unfollow any non-follower past grace period
onlyTracked: true,
// Dry run mode - just show who would be unfollowed
dryRun: false,
// ---- TIMING ----
unfollowDelay: 1500,
confirmDelay: 1000,
scrollDelay: 2000,
maxScrolls: 100,
maxRetries: 5
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function smartUnfollow() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $userCell = '[data-testid="UserCell"]';
const $unfollowBtn = '[data-testid$="-unfollow"]';
const $confirmBtn = '[data-testid="confirmationSheetConfirm"]';
const $followsYou = '[data-testid="userFollowIndicator"]';
const TRACKING_KEY = 'xactions_follow_tracking';
const FOLLOWERS_KEY = 'xactions_my_current_followers';
// Shared with protect-active-users.js and whitelist.js so a user marked
// protected/whitelisted by either script is never touched here.
const PROTECTED_KEY = 'xactions_protected_users';
const WHITELIST_KEY = 'xactions_whitelist';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🧠 SMART UNFOLLOW ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Detect page type
const url = window.location.href;
const isFollowersPage = url.includes('/followers');
const isFollowingPage = url.includes('/following');
if (!isFollowersPage && !isFollowingPage) {
console.error('❌ ERROR: Must be on your followers or following page!');
console.log('📍 Step 1: https://x.com/YOUR_USERNAME/followers');
console.log('📍 Step 2: https://x.com/YOUR_USERNAME/following');
return;
}
/**
* Get username from cell
*/
function getUsername(cell) {
const link = cell.querySelector('a[href^="/"]');
return link ? link.getAttribute('href').replace('/', '').split('/')[0] : null;
}
// ==========================================
// PHASE 1: Scan followers
// ==========================================
if (isFollowersPage) {
console.log('📋 PHASE 1: Scanning your followers...');
console.log('');
const followers = new Set();
let lastCount = 0;
let retries = 0;
let scrolls = 0;
while (scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
document.querySelectorAll($userCell).forEach(cell => {
const username = getUsername(cell);
if (username) followers.add(username);
});
if (followers.size === lastCount) {
retries++;
} else {
retries = 0;
lastCount = followers.size;
}
console.log(`📊 Found ${followers.size} followers...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
// Save followers
localStorage.setItem(FOLLOWERS_KEY, JSON.stringify({
savedAt: new Date().toISOString(),
followers: [...followers]
}));
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ PHASE 1 COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 Saved ${followers.size} followers`);
console.log('');
console.log('👉 Now go to your FOLLOWING page and run this script again!');
console.log(' https://x.com/YOUR_USERNAME/following');
console.log('');
return { phase: 1, followers: followers.size };
}
// ==========================================
// PHASE 2: Unfollow non-followers
// ==========================================
console.log('📋 PHASE 2: Unfollowing non-followers...');
console.log(`⏱️ Grace period: ${CONFIG.daysToWait} days`);
console.log(`📊 Max unfollows: ${CONFIG.maxUnfollows}`);
if (CONFIG.dryRun) console.log('🔍 DRY RUN MODE - no actual unfollows');
console.log('');
// Load followers
let myFollowers = new Set();
try {
const saved = localStorage.getItem(FOLLOWERS_KEY);
if (saved) {
const data = JSON.parse(saved);
data.followers.forEach(u => myFollowers.add(u));
console.log(`📚 Loaded ${myFollowers.size} followers from Phase 1`);
}
} catch (e) {}
if (myFollowers.size === 0) {
console.warn('⚠️ No followers data found!');
console.log('👉 Run this script on your FOLLOWERS page first!');
return;
}
// Load tracking data
let trackingData = {};
try {
const saved = localStorage.getItem(TRACKING_KEY);
if (saved) trackingData = JSON.parse(saved);
console.log(`📚 Loaded ${Object.keys(trackingData).length} tracked follows`);
} catch (e) {}
// Load protected users (from protect-active-users.js: engaged likers/
// repliers/retweeters) and the shared whitelist (from whitelist.js) so
// both lists are respected here too, not just CONFIG.whitelist.
const protectedUsers = new Set();
try {
const saved = localStorage.getItem(PROTECTED_KEY);
if (saved) {
const data = JSON.parse(saved);
(data.users || []).forEach(u => u.username && protectedUsers.add(u.username.toLowerCase()));
}
} catch (e) {}
try {
const saved = localStorage.getItem(WHITELIST_KEY);
if (saved) {
const list = JSON.parse(saved);
(Array.isArray(list) ? list : []).forEach(u => u.username && protectedUsers.add(u.username.toLowerCase()));
}
} catch (e) {}
if (protectedUsers.size > 0) {
console.log(`🛡️ Loaded ${protectedUsers.size} protected/whitelisted users (never unfollowed)`);
}
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - CONFIG.daysToWait);
let totalUnfollowed = 0;
let scrolls = 0;
let retries = 0;
const seenUsers = new Set();
console.log('');
console.log('🚀 Scanning following list...');
console.log('');
while (totalUnfollowed < CONFIG.maxUnfollows && scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
const cells = document.querySelectorAll($userCell);
let newUsersThisPass = 0;
for (const cell of cells) {
if (totalUnfollowed >= CONFIG.maxUnfollows) break;
const username = getUsername(cell);
if (!username) continue;
// Progress = new accounts appearing in the list, not just unfollow
// candidates. A run of mutuals must not end the session early.
if (!seenUsers.has(username)) {
seenUsers.add(username);
newUsersThisPass++;
}
// Skip whitelist (inline CONFIG list + shared whitelist.js / protect-active-users.js lists)
if (CONFIG.whitelist.includes(username.toLowerCase())) continue;
if (protectedUsers.has(username.toLowerCase())) continue;
// Check if follows you
if (myFollowers.has(username) || cell.querySelector($followsYou)) {
continue; // Mutual - keep
}
// Check tracking
const tracking = trackingData[username];
if (CONFIG.onlyTracked && !tracking) {
continue; // Not tracked, skip
}
// Check grace period
if (tracking) {
const followedDate = new Date(tracking.followedAt);
if (followedDate > cutoffDate) {
continue; // Still in grace period
}
}
// This user should be unfollowed
const unfollowBtn = cell.querySelector($unfollowBtn);
if (!unfollowBtn) continue;
if (CONFIG.dryRun) {
console.log(`🔍 Would unfollow: @${username}`);
totalUnfollowed++;
continue;
}
try {
unfollowBtn.click();
await sleep(500);
const confirmBtn = document.querySelector($confirmBtn);
if (confirmBtn) {
confirmBtn.click();
totalUnfollowed++;
console.log(`🚫 Unfollowed #${totalUnfollowed}: @${username}`);
// Remove from tracking
if (tracking) {
delete trackingData[username];
localStorage.setItem(TRACKING_KEY, JSON.stringify(trackingData));
}
await sleep(CONFIG.confirmDelay);
}
await sleep(CONFIG.unfollowDelay);
} catch (e) {
console.warn('⚠️ Error:', e.message);
}
}
if (newUsersThisPass === 0) {
retries++;
} else {
retries = 0;
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ SMART UNFOLLOW COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`🚫 Total unfollowed: ${totalUnfollowed}`);
console.log('');
return { phase: 2, unfollowed: totalUnfollowed };
})();
});
register("thread-unroller", function(){
var CONFIG = {
// Output format: 'markdown', 'text', 'json'
format: 'markdown',
// Include media URLs
includeMedia: true,
// Include engagement stats
includeStats: true,
// Maximum tweets to extract
maxTweets: 50,
// Scroll delay
scrollDelay: 1500,
// Auto-download
autoDownload: true,
// Copy to clipboard
copyToClipboard: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function threadUnroller() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📜 THREAD UNROLLER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify page
if (!window.location.href.includes('/status/')) {
console.error('❌ ERROR: Must be on a tweet page!');
console.log('📍 Go to any tweet in a thread');
return;
}
// Get thread author from URL
const pathMatch = window.location.pathname.match(/^\/([^\/]+)\/status/);
const threadAuthor = pathMatch ? pathMatch[1].toLowerCase() : null;
if (!threadAuthor) {
console.error('❌ Could not determine thread author');
return;
}
console.log(`👤 Thread author: @${threadAuthor}`);
console.log(`📋 Format: ${CONFIG.format}`);
console.log('');
console.log('🚀 Unrolling thread...');
console.log('');
const tweets = [];
const seenIds = new Set();
let scrolls = 0;
const maxScrolls = 30;
/**
* Extract tweet data
*/
function extractTweet(tweetEl) {
try {
// Get tweet ID from the timestamp's permalink anchor; the first
// /status/ link in the article can belong to a quoted tweet
const timeAnchor = tweetEl.querySelector('time')?.closest('a[href*="/status/"]');
const link = timeAnchor || tweetEl.querySelector('a[href*="/status/"]');
if (!link) return null;
const match = link.href.match(/\/status\/(\d+)/);
if (!match) return null;
const tweetId = match[1];
if (seenIds.has(tweetId)) return null;
// Check if from thread author
const authorLink = tweetEl.querySelector('div[data-testid="User-Name"] a[href^="/"]') ||
tweetEl.querySelector('a[href^="/"][role="link"]');
const tweetAuthor = authorLink ? authorLink.getAttribute('href').replace('/', '').split('/')[0].toLowerCase() : null;
if (tweetAuthor !== threadAuthor) return null;
seenIds.add(tweetId);
// Get text
const textEl = tweetEl.querySelector($tweetText);
const text = textEl ? textEl.innerText : '';
// Get timestamp
const timeEl = tweetEl.querySelector('time');
const timestamp = timeEl ? timeEl.getAttribute('datetime') : null;
const displayTime = timeEl ? timeEl.innerText : '';
// Get engagement
const getMetric = (testId) => {
const el = tweetEl.querySelector(`[data-testid="${testId}"]`);
const span = el?.querySelector('span span');
return span ? span.innerText : '0';
};
// Get images
const images = [];
tweetEl.querySelectorAll('[data-testid="tweetPhoto"] img').forEach(img => {
if (img.src && img.src.includes('pbs.twimg.com')) {
images.push(img.src);
}
});
return {
id: tweetId,
url: `https://x.com/${threadAuthor}/status/${tweetId}`,
text,
timestamp,
displayTime,
metrics: {
replies: getMetric('reply'),
retweets: getMetric('retweet'),
likes: getMetric('like')
},
images
};
} catch (e) {
return null;
}
}
// Scroll and extract
let lastCount = 0;
let noNewCount = 0;
while (tweets.length < CONFIG.maxTweets && scrolls < maxScrolls) {
document.querySelectorAll($tweet).forEach(el => {
const tweet = extractTweet(el);
if (tweet) tweets.push(tweet);
});
console.log(`📊 Extracted ${tweets.length} tweets from thread...`);
// Stop early once scrolling stops producing new tweets
if (tweets.length === lastCount) {
noNewCount++;
if (noNewCount >= 5) {
console.log('📭 No new tweets after 5 scrolls. End of thread.');
break;
}
} else {
noNewCount = 0;
lastCount = tweets.length;
}
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
// Sort chronologically (oldest first)
tweets.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
console.log('');
console.log(`✅ Extracted ${tweets.length} tweets from @${threadAuthor}'s thread`);
console.log('');
// Format output
let output;
let filename;
let mimeType;
if (CONFIG.format === 'markdown') {
let md = `# Thread by @${threadAuthor}\n\n`;
md += `> Unrolled on ${new Date().toISOString()}\n`;
md += `> ${tweets.length} tweets\n\n`;
md += `---\n\n`;
tweets.forEach((t, i) => {
md += `## ${i + 1}/${tweets.length}\n\n`;
md += `${t.text}\n\n`;
if (CONFIG.includeMedia && t.images.length > 0) {
t.images.forEach(img => {
md += `\n`;
});
md += '\n';
}
if (CONFIG.includeStats) {
md += `*${t.displayTime} • ❤️ ${t.metrics.likes} • 🔄 ${t.metrics.retweets} • 💬 ${t.metrics.replies}*\n\n`;
}
md += `[View tweet](${t.url})\n\n`;
md += `---\n\n`;
});
output = md;
filename = `thread_${threadAuthor}_${Date.now()}.md`;
mimeType = 'text/markdown';
} else if (CONFIG.format === 'text') {
let txt = `THREAD BY @${threadAuthor.toUpperCase()}\n`;
txt += `${'='.repeat(50)}\n`;
txt += `Unrolled: ${new Date().toISOString()}\n`;
txt += `Total tweets: ${tweets.length}\n`;
txt += `${'='.repeat(50)}\n\n`;
tweets.forEach((t, i) => {
txt += `[${i + 1}/${tweets.length}]\n`;
txt += `${'-'.repeat(30)}\n`;
txt += `${t.text}\n\n`;
if (CONFIG.includeStats) {
txt += `${t.displayTime}\n`;
txt += `Likes: ${t.metrics.likes} | RTs: ${t.metrics.retweets} | Replies: ${t.metrics.replies}\n`;
}
txt += `${t.url}\n`;
txt += `\n${'='.repeat(50)}\n\n`;
});
output = txt;
filename = `thread_${threadAuthor}_${Date.now()}.txt`;
mimeType = 'text/plain';
} else {
output = JSON.stringify({
author: threadAuthor,
unrolledAt: new Date().toISOString(),
tweetCount: tweets.length,
tweets
}, null, 2);
filename = `thread_${threadAuthor}_${Date.now()}.json`;
mimeType = 'application/json';
}
// Download
if (CONFIG.autoDownload) {
const blob = new Blob([output], { 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);
console.log(`💾 Downloaded: ${filename}`);
}
// Copy to clipboard
if (CONFIG.copyToClipboard) {
try {
await navigator.clipboard.writeText(output);
console.log('📋 Copied to clipboard!');
} catch (e) {}
}
// Display preview
console.log('');
console.log('📖 Thread preview:');
tweets.slice(0, 3).forEach((t, i) => {
console.log(` ${i + 1}. ${t.text.substring(0, 80)}...`);
});
if (tweets.length > 3) {
console.log(` ... and ${tweets.length - 3} more tweets`);
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ THREAD UNROLLED! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
window.unrolledThread = { author: threadAuthor, tweets };
console.log('💡 Access via: window.unrolledThread');
return { author: threadAuthor, tweets };
})();
});
register("trending-monitor", function(){
(async function trendingMonitor() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Highlight any trend whose name contains one of these words (case-insensitive).
watchKeywords: [],
// Re-scan interval in ms. Set to 0 for a single one-off scan.
// 300000 = every 5 minutes. Stop with window.stopTrendingMonitor().
repeatIntervalMs: 0,
// Scroll passes per scan to load the full trends list.
scrollRounds: 4,
// Delay between scrolls (ms).
scrollDelay: 1400,
// Auto-download a JSON snapshot after each scan.
exportResults: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
trend: '[data-testid="trend"]'
};
// ============================================
// 🗂️ CATEGORY KEYWORDS
// ============================================
const CATEGORIES = {
Tech: ['ai', 'chatgpt', 'openai', 'google', 'apple', 'microsoft', 'nvidia', 'coding', 'crypto', 'bitcoin', 'solana', 'web3', 'ethereum'],
Politics: ['trump', 'biden', 'congress', 'election', 'democrat', 'republican', 'government', 'senate'],
Sports: ['nfl', 'nba', 'mlb', 'soccer', 'football', 'basketball', 'fifa', 'ufc', 'f1', 'olympics'],
Entertainment: ['movie', 'music', 'album', 'netflix', 'disney', 'grammy', 'oscar', 'tour', 'concert'],
Business: ['stock', 'market', 'earnings', 'ipo', 'startup', 'economy', 'fed', 'inflation'],
Gaming: ['gaming', 'playstation', 'xbox', 'nintendo', 'steam', 'esports', 'minecraft']
};
const classify = (text) => {
if (!text) return 'Other';
const lower = text.toLowerCase();
for (const [category, words] of Object.entries(CATEGORIES)) {
if (words.some((w) => lower.includes(w))) return category;
}
return 'Other';
};
// ============================================
// 🛠️ HELPERS
// ============================================
const STORAGE_KEY = 'xactions_trends_history';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
const download = (data, filename) => {
try {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
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);
log.success(`Snapshot downloaded: ${filename}`);
} catch (e) {
log.error(`Could not download snapshot: ${e.message}`);
}
};
const parsePostCount = (text) => {
if (!text) return 0;
const match = text.match(/([\d,.]+)\s*([KMB])?/i);
if (!match) return 0;
let num = parseFloat(match[1].replace(/,/g, ''));
if (Number.isNaN(num)) return 0;
const mult = { K: 1e3, M: 1e6, B: 1e9 };
if (match[2]) num *= mult[match[2].toUpperCase()];
return Math.round(num);
};
const loadHistory = () => {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
} catch {
return [];
}
};
const saveSnapshot = (trends) => {
try {
const history = loadHistory();
history.push({
timestamp: new Date().toISOString(),
trends: trends.map((t) => ({ rank: t.rank, topic: t.topic, postCount: t.postCount, category: t.category }))
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(history.slice(-50)));
} catch (e) {
log.warning(`Could not save history: ${e.message}`);
}
};
// Read one trends scan from the DOM. Each trend cell holds several spans;
// the topic is the longest non-metadata span and the post count is the
// span that mentions "posts"/"Tweets".
const scrapeTrends = async () => {
const trends = [];
const seen = new Set();
for (let round = 0; round < CONFIG.scrollRounds; round++) {
document.querySelectorAll(SELECTORS.trend).forEach((cell) => {
let topic = '';
let postCountRaw = '';
cell.querySelectorAll('span').forEach((span) => {
const text = span.textContent.trim();
if (/[\d,.]+[KMB]?\s*(posts|tweets)/i.test(text)) {
postCountRaw = text;
return;
}
const isMeta = text.includes('·') || /Trending|Only on X|Promoted/i.test(text) || /posts|tweets/i.test(text) || /^\d+$/.test(text);
if (!isMeta && text.length > 1 && text.length < 80 && text.length > topic.length) {
topic = text;
}
});
if (!topic || seen.has(topic.toLowerCase())) return;
seen.add(topic.toLowerCase());
trends.push({
rank: trends.length + 1,
topic,
category: classify(topic),
postCount: parsePostCount(postCountRaw),
postCountRaw: postCountRaw || null
});
});
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
}
return trends;
};
const compareWithPrevious = (trends) => {
const history = loadHistory();
if (history.length < 2) return;
const prev = history[history.length - 2];
const prevMap = new Map(prev.trends.map((t) => [t.topic.toLowerCase(), t]));
const fresh = [];
const rising = [];
trends.forEach((t) => {
const p = prevMap.get(t.topic.toLowerCase());
if (!p) fresh.push(t);
else if (t.rank < p.rank) rising.push({ ...t, prevRank: p.rank, change: p.rank - t.rank });
});
if (fresh.length > 0) {
console.log('');
console.log(` 🆕 NEW since last scan (${fresh.length}):`);
fresh.slice(0, 8).forEach((t) => console.log(` #${t.rank} ${t.topic} [${t.category}]`));
}
if (rising.length > 0) {
console.log('');
console.log(` 📈 RISING (${rising.length}):`);
rising.sort((a, b) => b.change - a.change).slice(0, 5).forEach((t) => {
console.log(` ↑${t.change} ${t.topic} (#${t.prevRank} -> #${t.rank})`);
});
}
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const runScan = async (scanNo) => {
console.log('');
console.log('━'.repeat(60));
console.log(`📡 TRENDS SCAN ${scanNo} at ${new Date().toLocaleTimeString()}`);
console.log('━'.repeat(60));
const trends = await scrapeTrends();
if (trends.length === 0) {
log.error('No trends found. Go to x.com/explore/tabs/trending and make sure trends are visible.');
return null;
}
trends.forEach((t) => {
const count = t.postCountRaw ? ` (${t.postCountRaw})` : '';
console.log(` ${String(t.rank).padStart(2)}. [${t.category.padEnd(13)}] ${t.topic}${count}`);
});
// Category distribution.
const catCounts = {};
trends.forEach((t) => { catCounts[t.category] = (catCounts[t.category] || 0) + 1; });
console.log('');
console.log(' 📊 CATEGORY MIX:');
Object.entries(catCounts).sort((a, b) => b[1] - a[1]).forEach(([cat, count]) => {
const bar = '█'.repeat(Math.round((count / trends.length) * 20));
console.log(` ${cat.padEnd(14)} ${count} (${((count / trends.length) * 100).toFixed(0)}%) ${bar}`);
});
// Watched keyword highlights.
const matches = [];
if (CONFIG.watchKeywords.length > 0) {
trends.forEach((t) => {
CONFIG.watchKeywords.forEach((kw) => {
if (t.topic.toLowerCase().includes(kw.toLowerCase())) {
matches.push({ topic: t.topic, keyword: kw, rank: t.rank, postCount: t.postCount });
}
});
});
console.log('');
console.log(' 🔔 WATCHED KEYWORDS:');
if (matches.length === 0) {
console.log(' No watched keywords in the current trends.');
} else {
matches.forEach((m) => console.log(` 🚨 "${m.topic}" matches "${m.keyword}" (rank #${m.rank})`));
}
}
saveSnapshot(trends);
compareWithPrevious(trends);
const snapshot = {
scannedAt: new Date().toISOString(),
page: window.location.href,
totalTrends: trends.length,
watchKeywords: CONFIG.watchKeywords,
keywordMatches: matches,
categoryMix: catCounts,
trends
};
if (CONFIG.exportResults) {
console.log('');
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
download(snapshot, `xactions-trends-${stamp}.json`);
}
window.xactionsTrends = snapshot;
return snapshot;
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📡 TRENDING MONITOR - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
if (!/(^|\.)x\.com$/.test(location.hostname) && !/(^|\.)twitter\.com$/.test(location.hostname)) {
log.warning('Not on x.com. Open x.com/explore/tabs/trending and run again.');
return;
}
if (!/\/explore/.test(location.pathname)) {
log.warning('You are not on the Explore/Trends page. Go to x.com/explore/tabs/trending for the full list.');
log.info('Continuing anyway in case trends are rendered in a sidebar.');
}
if (CONFIG.watchKeywords.length > 0) log.info(`Watching keywords: ${CONFIG.watchKeywords.join(', ')}`);
// Stop switch for the repeating monitor.
let stopped = false;
window.stopTrendingMonitor = () => {
stopped = true;
log.warning('Stop requested. The monitor will exit after the current scan.');
};
// First scan always runs.
await runScan(1);
if (CONFIG.repeatIntervalMs > 0) {
log.info(`Repeat mode ON. Re-scanning every ${(CONFIG.repeatIntervalMs / 60000).toFixed(1)} min.`);
log.info('To stop: window.stopTrendingMonitor()');
let scanNo = 2;
while (!stopped) {
// Sleep in short slices so the stop switch responds quickly.
const slices = Math.max(1, Math.round(CONFIG.repeatIntervalMs / 1000));
for (let i = 0; i < slices && !stopped; i++) await sleep(1000);
if (stopped) break;
await runScan(scanNo++);
}
log.success('Trending monitor stopped.');
} else {
log.info('Single scan complete. Set CONFIG.repeatIntervalMs > 0 for continuous monitoring.');
}
return window.xactionsTrends;
})();
});
register("tweet-performance", function(){
(async function tweetPerformance() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// Maximum number of posts to collect.
maxPosts: 30,
// Ranking key for the printed table:
// 'likes' | 'reposts' | 'replies' | 'engagementRate'
// engagementRate = (likes + reposts + replies) / views, as a percentage.
sortBy: 'likes',
// How many rows to print in the top table.
topN: 10,
// Skip reposts (other people's tweets this account reposted).
excludeRetweets: true,
// Max scroll attempts before giving up on loading more posts.
maxScrollAttempts: 40,
// Delay between scrolls (ms). Increase if posts load slowly.
scrollDelay: 1800,
// Auto-download a JSON report when finished.
exportResults: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
socialContext: '[data-testid="socialContext"]',
actionGroup: '[role="group"]',
photo: '[data-testid="tweetPhoto"]',
video: '[data-testid="videoPlayer"], [data-testid="videoComponent"]',
card: '[data-testid="card.wrapper"]',
poll: '[data-testid="poll"]',
analytics: 'a[href*="/analytics"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`)
};
// Parse "1,234", "12.3K", "4.5M" into an integer.
const parseCount = (text) => {
if (!text) return 0;
const cleaned = text.trim().replace(/,/g, '');
const match = cleaned.match(/([\d.]+)\s*([KMB])?/i);
if (!match) return 0;
let num = parseFloat(match[1]);
if (Number.isNaN(num)) return 0;
const mult = { K: 1e3, M: 1e6, B: 1e9 };
if (match[2]) num *= mult[match[2].toUpperCase()];
return Math.round(num);
};
const fmt = (n) => (n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'K' : String(n));
const download = (data, filename) => {
try {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
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);
log.success(`Report downloaded: ${filename}`);
} catch (e) {
log.error(`Could not download report: ${e.message}`);
}
};
// Extract likes/reposts/replies/views from a tweet's action bar. The action
// buttons expose aria-labels that hold the real numbers even when the
// visible text is abbreviated.
const readMetrics = (article) => {
let replies = 0;
let reposts = 0;
let likes = 0;
let views = 0;
const group = article.querySelector(SELECTORS.actionGroup);
if (group) {
group.querySelectorAll('button, a').forEach((el) => {
const label = (el.getAttribute('aria-label') || '').toLowerCase();
if (!label) return;
const numMatch = label.match(/([\d,.]+)/);
const count = numMatch ? parseCount(numMatch[1]) : parseCount(el.textContent);
if (/repl/.test(label)) replies = count || replies;
else if (/repost|retweet/.test(label)) reposts = count || reposts;
else if (/like/.test(label)) likes = count || likes;
else if (/view/.test(label)) views = count || views;
});
}
if (views === 0) {
const viewLink = article.querySelector(SELECTORS.analytics);
if (viewLink) views = parseCount(viewLink.textContent);
}
return { replies, reposts, likes, views };
};
const getTweetId = (article) => {
const timeAnchor = article.querySelector('time')?.closest('a[href*="/status/"]');
const anchor = timeAnchor || article.querySelector('a[href*="/status/"]');
if (!anchor) return null;
const match = anchor.href.match(/\/status\/(\d+)/);
return match ? match[1] : null;
};
const isRetweet = (article) => {
const ctx = article.querySelector(SELECTORS.socialContext);
return !!ctx && ctx.closest('a') !== null;
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🎯 TWEET PERFORMANCE - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
const pathHandle = location.pathname.match(/^\/([A-Za-z0-9_]+)/)?.[1];
const reserved = ['home', 'explore', 'notifications', 'messages', 'i', 'settings', 'search'];
if (!pathHandle || reserved.includes(pathHandle)) {
log.warning('Not on a profile page. Go to x.com/<username> (e.g. your own profile) and run again.');
return;
}
const validSorts = ['likes', 'reposts', 'replies', 'engagementRate'];
if (!validSorts.includes(CONFIG.sortBy)) {
log.warning(`Unknown sortBy "${CONFIG.sortBy}". Falling back to "likes".`);
CONFIG.sortBy = 'likes';
}
log.info(`Profile: @${pathHandle}`);
log.info(`Collecting up to ${CONFIG.maxPosts} posts, ranking by ${CONFIG.sortBy}`);
log.info('To stop early: window.stopTweetPerformance()');
console.log('');
// Stop switch for the scroll loop.
let stopped = false;
window.stopTweetPerformance = () => {
stopped = true;
log.warning('Stop requested. Finishing the current pass, then reporting.');
};
const posts = new Map();
let scrollAttempts = 0;
let stalls = 0;
const collect = () => {
document.querySelectorAll(SELECTORS.tweet).forEach((article) => {
try {
if (CONFIG.excludeRetweets && isRetweet(article)) return;
const id = getTweetId(article);
if (!id || posts.has(id)) return;
const text = (article.querySelector(SELECTORS.tweetText)?.textContent || '').trim();
const metrics = readMetrics(article);
const hasImage = !!article.querySelector(SELECTORS.photo);
const hasVideo = !!article.querySelector(SELECTORS.video);
const hasCard = !!article.querySelector(SELECTORS.card);
const hasPoll = !!article.querySelector(SELECTORS.poll);
const type = hasPoll ? 'poll' : hasVideo ? 'video' : hasImage ? 'image' : hasCard ? 'link' : 'text';
const engagement = metrics.likes + metrics.reposts + metrics.replies;
const engagementRate = metrics.views > 0
? Math.round((engagement / metrics.views) * 10000) / 100
: 0;
const timeEl = article.querySelector('time');
posts.set(id, {
id,
url: `https://x.com/${pathHandle}/status/${id}`,
text: text.slice(0, 280),
type,
likes: metrics.likes,
reposts: metrics.reposts,
replies: metrics.replies,
views: metrics.views,
engagement,
engagementRate,
hashtags: text.match(/#\w+/g) || [],
timestamp: timeEl?.getAttribute('datetime') || null
});
} catch (e) {
log.warning(`Skipped a post: ${e.message}`);
}
});
};
while (!stopped && posts.size < CONFIG.maxPosts && scrollAttempts < CONFIG.maxScrollAttempts) {
const before = posts.size;
collect();
if (posts.size > before) {
log.info(`Collected ${posts.size}/${CONFIG.maxPosts} posts`);
stalls = 0;
} else {
stalls++;
if (stalls >= 5) {
log.warning('No new posts after several scrolls. Reached the end of what loaded.');
break;
}
}
if (posts.size >= CONFIG.maxPosts) break;
window.scrollTo(0, document.body.scrollHeight);
scrollAttempts++;
await sleep(CONFIG.scrollDelay);
}
collect();
const tweets = [...posts.values()].slice(0, CONFIG.maxPosts);
if (tweets.length === 0) {
log.error('No posts found. Make sure the profile has visible posts and try scrolling once manually.');
return;
}
// Rankings.
const sortKey = CONFIG.sortBy;
const ranked = [...tweets].sort((a, b) => b[sortKey] - a[sortKey]);
const sum = (key) => tweets.reduce((s, t) => s + t[key], 0);
const avg = (key) => Math.round(sum(key) / tweets.length);
const totalEngagement = sum('engagement');
const byType = {};
tweets.forEach((t) => {
byType[t.type] = byType[t.type] || { count: 0, engagement: 0 };
byType[t.type].count++;
byType[t.type].engagement += t.engagement;
});
// ============================================
// 📊 SUMMARY
// ============================================
const labelFor = { likes: '❤️ Likes', reposts: '🔁 Reposts', replies: '💬 Replies', engagementRate: '📊 Eng. rate' };
console.log('');
console.log('━'.repeat(64));
console.log(`🏆 TOP ${Math.min(CONFIG.topN, ranked.length)} POSTS BY ${labelFor[sortKey].toUpperCase()} (@${pathHandle})`);
console.log('━'.repeat(64));
console.log(` # ❤️ Likes 🔁 RT 💬 Rep 👀 Views Rate% Post`);
console.log(' ' + '─'.repeat(60));
ranked.slice(0, CONFIG.topN).forEach((t, i) => {
const preview = (t.text.replace(/\s+/g, ' ').slice(0, 40) || '(no text)');
console.log(
` ${String(i + 1).padEnd(3)} ${fmt(t.likes).padEnd(9)} ${fmt(t.reposts).padEnd(9)} ${fmt(t.replies).padEnd(9)} ${fmt(t.views).padEnd(10)} ${String(t.engagementRate).padEnd(6)} ${preview}`
);
});
console.log('');
console.log('📊 AVERAGES (across ' + tweets.length + ' posts):');
console.log(` ❤️ ${fmt(avg('likes'))} likes 🔁 ${fmt(avg('reposts'))} reposts 💬 ${fmt(avg('replies'))} replies 👀 ${fmt(avg('views'))} views`);
console.log('');
console.log('📐 BY CONTENT TYPE (avg engagement):');
Object.entries(byType)
.map(([type, d]) => [type, Math.round(d.engagement / d.count), d.count])
.sort((a, b) => b[1] - a[1])
.forEach(([type, avgEng, count]) => {
console.log(` ${type.padEnd(6)} avg ${fmt(avgEng).padEnd(8)} engagement (${count} post${count === 1 ? '' : 's'})`);
});
const withMedia = tweets.filter((t) => t.type === 'image' || t.type === 'video');
const textOnly = tweets.filter((t) => t.type === 'text');
const avgOf = (arr) => (arr.length ? Math.round(arr.reduce((s, t) => s + t.engagement, 0) / arr.length) : 0);
console.log('');
console.log(`📸 Media posts avg ${fmt(avgOf(withMedia))} eng (${withMedia.length}) vs Text posts avg ${fmt(avgOf(textOnly))} eng (${textOnly.length})`);
const report = {
profile: pathHandle,
profileUrl: `https://x.com/${pathHandle}`,
sortedBy: sortKey,
analyzedAt: new Date().toISOString(),
summary: {
totalPosts: tweets.length,
totalEngagement,
avgLikes: avg('likes'),
avgReposts: avg('reposts'),
avgReplies: avg('replies'),
avgViews: avg('views'),
byType
},
ranked,
posts: tweets
};
if (CONFIG.exportResults) {
console.log('');
download(report, `xactions-performance-${pathHandle}-${new Date().toISOString().slice(0, 10)}.json`);
}
window.xactionsPerformance = report;
console.log('');
log.info('Full report object: window.xactionsPerformance');
log.success('Done.');
return report;
})();
});
register("unfollow-everyone", function(){
var CONFIG = {
// Maximum retry attempts when no buttons found
// 💡 Increase if you have many accounts (takes longer to scroll)
maxRetries: 5,
// Delay between unfollows (milliseconds)
// 💡 Increase to 3000-5000 if getting rate limited
unfollowDelay: 1500,
// Delay after clicking confirm button
confirmDelay: 1000,
// Delay for scrolling to load more
scrollDelay: 2000,
// Stop after unfollowing this many (0 = unlimited)
// 💡 Set to 50-100 for testing first
maxUnfollows: 0,
// Show confirmation prompt before starting
confirmStart: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function unfollowEveryone() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $unfollowBtn = '[data-testid$="-unfollow"]';
const $confirmBtn = '[data-testid="confirmationSheetConfirm"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🚫 UNFOLLOW EVERYONE ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on the right page
if (!window.location.href.includes('/following')) {
console.error('❌ ERROR: You must be on your Following page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME/following');
return;
}
// Confirmation prompt
if (CONFIG.confirmStart) {
const confirmed = confirm(
'⚠️ WARNING: This will unfollow EVERYONE!\n\n' +
'Are you sure you want to continue?\n\n' +
'Click OK to start unfollowing.'
);
if (!confirmed) {
console.log('❌ Cancelled by user.');
return;
}
}
console.log('🚀 Starting mass unfollow...');
console.log('💡 To stop early: window.stopUnfollow()');
console.log('');
let totalUnfollowed = 0;
let retries = 0;
let stopped = false;
window.stopUnfollow = () => {
stopped = true;
console.log('🛑 Stopping after the current unfollow...');
};
while (retries < CONFIG.maxRetries && !stopped) {
// Scroll to bottom to load more users
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
// Find all unfollow buttons
const buttons = document.querySelectorAll($unfollowBtn);
if (buttons.length === 0) {
retries++;
console.log(`⏳ No buttons found. Retry ${retries}/${CONFIG.maxRetries}...`);
await sleep(CONFIG.scrollDelay);
continue;
}
let confirmedThisPass = 0;
for (const btn of buttons) {
if (stopped) break;
// Check max unfollows limit
if (CONFIG.maxUnfollows > 0 && totalUnfollowed >= CONFIG.maxUnfollows) {
console.log(`\n✅ Reached limit of ${CONFIG.maxUnfollows} unfollows!`);
console.log(`📊 Total unfollowed: ${totalUnfollowed}`);
delete window.stopUnfollow;
return { total: totalUnfollowed };
}
try {
// Click unfollow button
btn.click();
await sleep(500);
// Click confirmation
const confirmBtn = document.querySelector($confirmBtn);
if (confirmBtn) {
confirmBtn.click();
totalUnfollowed++;
confirmedThisPass++;
console.log(`✅ Unfollowed #${totalUnfollowed}`);
await sleep(CONFIG.confirmDelay);
}
await sleep(CONFIG.unfollowDelay);
} catch (e) {
console.warn('⚠️ Error unfollowing:', e.message);
}
}
// Only reset retries on real progress; otherwise a button that never
// confirms would keep this loop spinning forever
if (confirmedThisPass > 0) {
retries = 0;
} else {
retries++;
console.log(`⏳ No unfollows confirmed this pass. Retry ${retries}/${CONFIG.maxRetries}...`);
}
}
delete window.stopUnfollow;
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(stopped ? '║ 🛑 STOPPED BY USER ║' : '║ ✅ COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`📊 Total unfollowed: ${totalUnfollowed}`);
console.log('');
return { total: totalUnfollowed };
})();
});
register("unfollow-non-followers", function(){
var CONFIG = {
// Maximum retry attempts when no buttons found
maxRetries: 5,
// Delay between unfollows (milliseconds)
// 💡 Increase to 3000-5000 if getting rate limited
unfollowDelay: 1500,
// Delay after clicking confirm button
confirmDelay: 1000,
// Delay for scrolling to load more
scrollDelay: 2000,
// Stop after unfollowing this many (0 = unlimited)
// 💡 Set to 50-100 for testing first
maxUnfollows: 0,
// Show confirmation prompt before starting
confirmStart: true,
// Log kept users (mutual followers)
logKept: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function unfollowNonFollowers() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $unfollowBtn = '[data-testid$="-unfollow"]';
const $confirmBtn = '[data-testid="confirmationSheetConfirm"]';
const $followsYou = '[data-testid="userFollowIndicator"]'; // "Follows you" badge
const $userCell = '[data-testid="UserCell"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔄 UNFOLLOW NON-FOLLOWERS ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on the right page
if (!window.location.href.includes('/following')) {
console.error('❌ ERROR: You must be on your Following page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME/following');
return;
}
// Confirmation prompt
if (CONFIG.confirmStart) {
const confirmed = confirm(
'🔄 UNFOLLOW NON-FOLLOWERS\n\n' +
'This will unfollow accounts that don\'t follow you back.\n' +
'Mutual followers will be kept.\n\n' +
'Click OK to start.'
);
if (!confirmed) {
console.log('❌ Cancelled by user.');
return;
}
}
console.log('🚀 Starting to unfollow non-followers...');
console.log('💡 Accounts with "Follows you" badge will be kept.');
console.log('💡 To stop early: window.stopUnfollow()');
console.log('');
let totalUnfollowed = 0;
let totalKept = 0;
let retries = 0;
let stopped = false;
const seenUsers = new Set();
window.stopUnfollow = () => {
stopped = true;
console.log('🛑 Stopping after the current unfollow...');
};
while (retries < CONFIG.maxRetries && !stopped) {
// Scroll to bottom to load more users
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
// Find all unfollow buttons
const buttons = document.querySelectorAll($unfollowBtn);
if (buttons.length === 0) {
retries++;
console.log(`⏳ No buttons found. Retry ${retries}/${CONFIG.maxRetries}...`);
await sleep(CONFIG.scrollDelay);
continue;
}
let progressThisPass = 0;
for (const btn of buttons) {
if (stopped) break;
// Check max unfollows limit
if (CONFIG.maxUnfollows > 0 && totalUnfollowed >= CONFIG.maxUnfollows) {
console.log(`\n✅ Reached limit of ${CONFIG.maxUnfollows} unfollows!`);
delete window.stopUnfollow;
logSummary();
return;
}
try {
// Find the parent UserCell to check for "Follows you" badge
const userCell = btn.closest($userCell);
// Track accounts by handle so re-rendered cells aren't recounted and
// so a tail of mutual followers can't keep this loop alive forever
const userLink = userCell?.querySelector('a[href^="/"]');
const username = userLink ? userLink.getAttribute('href').replace('/', '').split('/')[0] : null;
const isNewUser = username && !seenUsers.has(username);
if (isNewUser) {
seenUsers.add(username);
progressThisPass++;
}
if (userCell) {
// Check if this user follows you back
const followsYou = userCell.querySelector($followsYou);
if (followsYou) {
// This user follows you - KEEP them (count each account once)
if (isNewUser || !username) {
totalKept++;
if (CONFIG.logKept) {
const nameEl = userCell.querySelector('[dir="ltr"] span');
const name = nameEl ? nameEl.textContent : 'Unknown';
console.log(`💚 Keeping: ${name} (follows you)`);
}
}
continue;
}
}
// User doesn't follow back - unfollow them
btn.click();
await sleep(500);
// Click confirmation
const confirmBtn = document.querySelector($confirmBtn);
if (confirmBtn) {
confirmBtn.click();
totalUnfollowed++;
progressThisPass++;
// Try to get username
const nameEl = userCell?.querySelector('[dir="ltr"] span');
const name = nameEl ? nameEl.textContent : `User #${totalUnfollowed}`;
console.log(`🚫 Unfollowed: ${name}`);
await sleep(CONFIG.confirmDelay);
}
await sleep(CONFIG.unfollowDelay);
} catch (e) {
console.warn('⚠️ Error:', e.message);
}
}
// Only reset retries on real progress (new accounts seen or unfollows
// done); visible mutual cells alone must not keep the loop spinning
if (progressThisPass > 0) {
retries = 0;
} else {
retries++;
console.log(`⏳ No new accounts this pass. Retry ${retries}/${CONFIG.maxRetries}...`);
}
}
delete window.stopUnfollow;
function logSummary() {
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(stopped ? '║ 🛑 STOPPED BY USER ║' : '║ ✅ COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`🚫 Unfollowed (non-followers): ${totalUnfollowed}`);
console.log(`💚 Kept (mutual followers): ${totalKept}`);
console.log('');
}
logSummary();
return { unfollowed: totalUnfollowed, kept: totalKept };
})();
});
register("unfollow-with-log", function(){
var CONFIG = {
// Maximum retry attempts
maxRetries: 5,
// Delay between unfollows (milliseconds)
unfollowDelay: 1500,
// Delay after clicking confirm
confirmDelay: 1000,
// Delay for scrolling
scrollDelay: 2000,
// Stop after this many unfollows (0 = unlimited)
maxUnfollows: 0,
// Auto-download log file when complete
autoDownload: true,
// Include timestamp in log
includeTimestamp: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function unfollowWithLog() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $unfollowBtn = '[data-testid$="-unfollow"]';
const $confirmBtn = '[data-testid="confirmationSheetConfirm"]';
const $followsYou = '[data-testid="userFollowIndicator"]';
const $userCell = '[data-testid="UserCell"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 📝 UNFOLLOW NON-FOLLOWERS WITH LOG ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify page
if (!window.location.href.includes('/following')) {
console.error('❌ ERROR: You must be on your Following page!');
console.log('📍 Go to: https://x.com/YOUR_USERNAME/following');
return;
}
const confirmed = confirm(
'📝 UNFOLLOW WITH LOG\n\n' +
'This will unfollow non-followers and save a log file.\n\n' +
'Click OK to start.'
);
if (!confirmed) {
console.log('❌ Cancelled by user.');
return;
}
console.log('🚀 Starting...');
console.log('💡 To stop early: window.stopUnfollow()');
console.log('');
const unfollowedList = [];
const keptList = [];
let retries = 0;
let stopped = false;
const startTime = new Date();
const seenUsers = new Set();
window.stopUnfollow = () => {
stopped = true;
console.log('🛑 Stopping after the current unfollow...');
};
/**
* Extract username from user cell
*/
function getUsername(userCell) {
if (!userCell) return null;
const link = userCell.querySelector('a[href^="/"]');
if (link) {
const href = link.getAttribute('href');
return href ? href.replace('/', '') : null;
}
return null;
}
/**
* Extract display name from user cell
*/
function getDisplayName(userCell) {
if (!userCell) return 'Unknown';
const nameSpan = userCell.querySelector('[dir="ltr"] span');
return nameSpan ? nameSpan.textContent : 'Unknown';
}
while (retries < CONFIG.maxRetries && !stopped) {
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
const buttons = document.querySelectorAll($unfollowBtn);
if (buttons.length === 0) {
retries++;
console.log(`⏳ No buttons found. Retry ${retries}/${CONFIG.maxRetries}...`);
await sleep(CONFIG.scrollDelay);
continue;
}
let progressThisPass = 0;
for (const btn of buttons) {
if (stopped) break;
if (CONFIG.maxUnfollows > 0 && unfollowedList.length >= CONFIG.maxUnfollows) {
console.log(`\n✅ Reached limit of ${CONFIG.maxUnfollows} unfollows!`);
delete window.stopUnfollow;
await downloadLog();
return;
}
try {
const userCell = btn.closest($userCell);
const username = getUsername(userCell);
const displayName = getDisplayName(userCell);
// Track accounts by handle so re-rendered cells aren't logged twice
// and a tail of mutual followers can't keep this loop alive forever
const isNewUser = username && !seenUsers.has(username);
if (isNewUser) {
seenUsers.add(username);
progressThisPass++;
}
// Check if follows you
if (userCell?.querySelector($followsYou)) {
if (isNewUser || !username) {
keptList.push({ username, displayName });
console.log(`💚 Keeping: @${username} (${displayName})`);
}
continue;
}
// Unfollow
btn.click();
await sleep(500);
const confirmBtn = document.querySelector($confirmBtn);
if (confirmBtn) {
confirmBtn.click();
const timestamp = new Date().toISOString();
unfollowedList.push({
username,
displayName,
timestamp
});
console.log(`🚫 Unfollowed #${unfollowedList.length}: @${username} (${displayName})`);
progressThisPass++;
await sleep(CONFIG.confirmDelay);
}
await sleep(CONFIG.unfollowDelay);
} catch (e) {
console.warn('⚠️ Error:', e.message);
}
}
// Only reset retries on real progress (new accounts seen or unfollows
// done); visible mutual cells alone must not keep the loop spinning
if (progressThisPass > 0) {
retries = 0;
} else {
retries++;
console.log(`⏳ No new accounts this pass. Retry ${retries}/${CONFIG.maxRetries}...`);
}
}
delete window.stopUnfollow;
await downloadLog();
/**
* Download the log file
*/
async function downloadLog() {
const endTime = new Date();
const duration = Math.round((endTime - startTime) / 1000 / 60);
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log(stopped ? '║ 🛑 STOPPED BY USER ║' : '║ ✅ COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`🚫 Unfollowed: ${unfollowedList.length}`);
console.log(`💚 Kept (mutual): ${keptList.length}`);
console.log(`⏱️ Duration: ${duration} minutes`);
console.log('');
if (CONFIG.autoDownload && unfollowedList.length > 0) {
// Create log content
let logContent = `UNFOLLOW LOG\n`;
logContent += `${'='.repeat(50)}\n`;
logContent += `Date: ${startTime.toISOString()}\n`;
logContent += `Total Unfollowed: ${unfollowedList.length}\n`;
logContent += `Total Kept (Mutual): ${keptList.length}\n`;
logContent += `Duration: ${duration} minutes\n`;
logContent += `${'='.repeat(50)}\n\n`;
logContent += `UNFOLLOWED ACCOUNTS:\n`;
logContent += `${'-'.repeat(30)}\n`;
unfollowedList.forEach((u, i) => {
let line = `${i + 1}. @${u.username} (${u.displayName})`;
if (CONFIG.includeTimestamp) {
line += ` - ${u.timestamp}`;
}
logContent += line + '\n';
});
logContent += `\n\nKEPT ACCOUNTS (MUTUAL FOLLOWERS):\n`;
logContent += `${'-'.repeat(30)}\n`;
keptList.forEach((u, i) => {
logContent += `${i + 1}. @${u.username} (${u.displayName})\n`;
});
// Download
const blob = new Blob([logContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `unfollowed_${startTime.toISOString().split('T')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('💾 Log file downloaded!');
}
// Store in window for access
window.unfollowLog = { unfollowed: unfollowedList, kept: keptList };
console.log('💡 Access data via: window.unfollowLog');
}
return { unfollowed: unfollowedList, kept: keptList };
})();
});
register("unlike-all", function(){
(async function unlikeAllTweets() {
'use strict';
// ═══════════════════════════════════════════════════════════════════════════
// 🎛️ CONFIGURATION - Customize these settings
// ═══════════════════════════════════════════════════════════════════════════
const CONFIG = {
maxUnlikes: 1000, // Maximum number of tweets to unlike (set to Infinity for all)
minDelay: 1000, // Minimum delay between unlikes (ms)
maxDelay: 2500, // Maximum delay between unlikes (ms)
scrollDelay: 1500, // Delay after scrolling to load more tweets (ms)
confirmBeforeStart: true, // Show confirmation dialog before starting
maxScrollAttempts: 5, // Max scroll attempts when no new tweets load
logProgress: true // Log progress every N unlikes
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ═══════════════════════════════════════════════════════════════════════════
// 🎯 SELECTORS
// ═══════════════════════════════════════════════════════════════════════════
const SELECTORS = {
unlikeButton: '[data-testid="unlike"]',
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]'
};
// ═══════════════════════════════════════════════════════════════════════════
// 🛠️ HELPERS
// ═══════════════════════════════════════════════════════════════════════════
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
return Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
};
const getUnlikeButtons = () => {
return document.querySelectorAll(SELECTORS.unlikeButton);
};
const scrollToBottom = () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
};
const formatTime = (seconds) => {
if (seconds < 60) return `${seconds}s`;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs}s`;
};
// ═══════════════════════════════════════════════════════════════════════════
// 🚀 MAIN SCRIPT
// ═══════════════════════════════════════════════════════════════════════════
console.clear();
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 💔 UNLIKE ALL TWEETS - XActions ║
║ by nichxbt ║
╚══════════════════════════════════════════════════════════════╝
`);
// Verify we're on the likes page
if (!window.location.href.includes('/likes')) {
console.error('❌ Error: Please navigate to your likes page first!');
console.log('📍 Go to: x.com/YOUR_USERNAME/likes');
return;
}
console.log('📋 Configuration:');
console.log(` • Max unlikes: ${CONFIG.maxUnlikes === Infinity ? 'Unlimited' : CONFIG.maxUnlikes}`);
console.log(` • Delay range: ${CONFIG.minDelay}ms - ${CONFIG.maxDelay}ms`);
console.log('');
// Confirmation dialog
if (CONFIG.confirmBeforeStart) {
const confirmed = confirm(
`💔 Unlike All Tweets\n\n` +
`This will unlike up to ${CONFIG.maxUnlikes === Infinity ? 'ALL' : CONFIG.maxUnlikes} tweets.\n\n` +
`⚠️ This action cannot be easily undone!\n\n` +
`Click OK to proceed or Cancel to abort.`
);
if (!confirmed) {
console.log('🛑 Operation cancelled by user.');
return;
}
}
console.log('🚀 Starting unlike process...');
console.log('💡 To stop early: window.stopUnlike()');
console.log('');
// Stats tracking
const stats = {
unliked: 0,
errors: 0,
startTime: Date.now()
};
let scrollAttempts = 0;
let lastButtonCount = 0;
let stopped = false;
window.stopUnlike = () => {
stopped = true;
console.log('🛑 Stopping after the current unlike...');
};
// Main loop
while (stats.unliked < CONFIG.maxUnlikes && !stopped) {
const unlikeButtons = getUnlikeButtons();
if (unlikeButtons.length === 0) {
// No unlike buttons found, try scrolling to load more
if (scrollAttempts >= CONFIG.maxScrollAttempts) {
console.log('📭 No more liked tweets found after multiple scroll attempts.');
break;
}
console.log(`📜 Scrolling to load more tweets... (attempt ${scrollAttempts + 1}/${CONFIG.maxScrollAttempts})`);
scrollToBottom();
await sleep(CONFIG.scrollDelay);
scrollAttempts++;
continue;
}
// Reset scroll attempts if we found new buttons
if (unlikeButtons.length !== lastButtonCount) {
scrollAttempts = 0;
lastButtonCount = unlikeButtons.length;
}
// Get the first unlike button
const button = unlikeButtons[0];
try {
// Scroll button into view
button.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(300);
// Click the unlike button
button.click();
stats.unliked++;
// Log progress
if (CONFIG.logProgress && stats.unliked % 10 === 0) {
const elapsed = Math.floor((Date.now() - stats.startTime) / 1000);
console.log(`💔 Progress: ${stats.unliked} unliked | ⏱️ ${formatTime(elapsed)} elapsed`);
} else if (stats.unliked % 5 === 0) {
console.log(`💔 Unliked: ${stats.unliked}`);
}
// Random delay before next action
await sleep(randomDelay());
} catch (error) {
stats.errors++;
console.warn(`⚠️ Error unliking tweet: ${error.message}`);
await sleep(1000);
}
// Check if we've hit the limit
if (stats.unliked >= CONFIG.maxUnlikes) {
console.log(`🎯 Reached maximum unlike limit (${CONFIG.maxUnlikes})`);
break;
}
}
delete window.stopUnlike;
// ═══════════════════════════════════════════════════════════════════════════
// 📊 COMPLETION SUMMARY
// ═══════════════════════════════════════════════════════════════════════════
const totalTime = Math.floor((Date.now() - stats.startTime) / 1000);
const avgTime = stats.unliked > 0 ? (totalTime / stats.unliked).toFixed(2) : 0;
console.log('');
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ ✅ UNLIKE ALL COMPLETE ║
╠══════════════════════════════════════════════════════════════╣
║ 📊 Results: ║
║ 💔 Tweets unliked: ${String(stats.unliked).padEnd(37)}║
║ ❌ Errors: ${String(stats.errors).padEnd(45)}║
║ ⏱️ Total time: ${formatTime(totalTime).padEnd(40)}║
║ 📈 Avg time/unlike: ${String(avgTime + 's').padEnd(36)}║
╚══════════════════════════════════════════════════════════════╝
`);
console.log('👋 Thanks for using XActions! Follow @nichxbt for updates.');
return stats;
})();
});
register("unlike-old", function(){
(async function unlikeOldTweets() {
'use strict';
// ═══════════════════════════════════════════════════════════════════════════
// 🎛️ CONFIGURATION - Customize these settings
// ═══════════════════════════════════════════════════════════════════════════
const CONFIG = {
daysOld: 30, // Only unlike tweets older than this many days
maxUnlikes: 500, // Maximum number of tweets to unlike (set to Infinity for all)
minDelay: 1000, // Minimum delay between unlikes (ms)
maxDelay: 2500, // Maximum delay between unlikes (ms)
scrollDelay: 1500, // Delay after scrolling to load more tweets (ms)
maxScrollAttempts: 10, // Max scroll attempts when no new old tweets found
logProgress: true // Log progress updates
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ═══════════════════════════════════════════════════════════════════════════
// 🎯 SELECTORS
// ═══════════════════════════════════════════════════════════════════════════
const SELECTORS = {
unlikeButton: '[data-testid="unlike"]',
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
timestamp: 'time[datetime]'
};
// ═══════════════════════════════════════════════════════════════════════════
// 🛠️ HELPERS
// ═══════════════════════════════════════════════════════════════════════════
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
return Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
};
const formatTime = (seconds) => {
if (seconds < 60) return `${seconds}s`;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs}s`;
};
const scrollToBottom = () => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
};
const getTweetAge = (tweetElement) => {
const timeElement = tweetElement.querySelector(SELECTORS.timestamp);
if (!timeElement) return null;
const tweetDate = new Date(timeElement.getAttribute('datetime'));
const now = new Date();
const diffMs = now - tweetDate;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return {
days: diffDays,
date: tweetDate,
isOld: diffDays >= CONFIG.daysOld
};
};
const findOldTweetsWithUnlike = () => {
const tweets = document.querySelectorAll(SELECTORS.tweet);
const oldTweets = [];
tweets.forEach(tweet => {
const unlikeButton = tweet.querySelector(SELECTORS.unlikeButton);
if (!unlikeButton) return;
const age = getTweetAge(tweet);
if (age && age.isOld) {
oldTweets.push({
element: tweet,
button: unlikeButton,
age: age
});
}
});
return oldTweets;
};
// ═══════════════════════════════════════════════════════════════════════════
// 🚀 MAIN SCRIPT
// ═══════════════════════════════════════════════════════════════════════════
console.clear();
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ 📅 UNLIKE OLD TWEETS - XActions ║
║ by nichxbt ║
╚══════════════════════════════════════════════════════════════╝
`);
// Verify we're on the likes page
if (!window.location.href.includes('/likes')) {
console.error('❌ Error: Please navigate to your likes page first!');
console.log('📍 Go to: x.com/YOUR_USERNAME/likes');
return;
}
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - CONFIG.daysOld);
console.log('📋 Configuration:');
console.log(` • Unlike tweets older than: ${CONFIG.daysOld} days`);
console.log(` • Cutoff date: ${cutoffDate.toLocaleDateString()}`);
console.log(` • Max unlikes: ${CONFIG.maxUnlikes === Infinity ? 'Unlimited' : CONFIG.maxUnlikes}`);
console.log(` • Delay range: ${CONFIG.minDelay}ms - ${CONFIG.maxDelay}ms`);
console.log('');
console.log('🚀 Starting unlike process for old tweets...');
console.log('💡 To stop early: window.stopUnlike()');
console.log('');
// Stats tracking
const stats = {
unliked: 0,
skipped: 0,
errors: 0,
startTime: Date.now(),
processedTweets: new Set()
};
let scrollAttempts = 0;
let consecutiveNoOldTweets = 0;
let stopped = false;
window.stopUnlike = () => {
stopped = true;
console.log('🛑 Stopping after the current unlike...');
};
// Main loop
while (stats.unliked < CONFIG.maxUnlikes && !stopped) {
// Find old tweets with unlike buttons
const oldTweets = findOldTweetsWithUnlike();
// Filter out already processed tweets
const newOldTweets = oldTweets.filter(t => {
const tweetId = t.element.getAttribute('aria-labelledby') ||
t.element.textContent.substring(0, 50);
return !stats.processedTweets.has(tweetId);
});
if (newOldTweets.length === 0) {
consecutiveNoOldTweets++;
// Scroll to load more tweets. The Likes feed is sorted by like-recency,
// not tweet age, so a long run of not-yet-old tweets is expected and
// must not be treated as "reached the end" - only a feed that has
// truly stopped growing (no new tweet articles after scrolling) means
// there's nothing left to find.
const tweetCountBefore = document.querySelectorAll(SELECTORS.tweet).length;
// Check if we've been scrolling with no results
if (consecutiveNoOldTweets >= 3) {
console.log(`📜 Scrolling to find older tweets... (attempt ${scrollAttempts + 1}/${CONFIG.maxScrollAttempts})`);
}
scrollToBottom();
await sleep(CONFIG.scrollDelay);
const tweetCountAfter = document.querySelectorAll(SELECTORS.tweet).length;
if (tweetCountAfter > tweetCountBefore) {
// Feed is still growing - real progress, even though none of the
// newly-loaded tweets qualified as "old" yet.
scrollAttempts = 0;
} else {
scrollAttempts++;
if (scrollAttempts >= CONFIG.maxScrollAttempts) {
console.log('📭 Reached the end of the Likes feed. No more old tweets found.');
break;
}
}
continue;
}
// Reset counters when we find old tweets
scrollAttempts = 0;
consecutiveNoOldTweets = 0;
// Process the first old tweet
const { element, button, age } = newOldTweets[0];
const tweetId = element.getAttribute('aria-labelledby') ||
element.textContent.substring(0, 50);
stats.processedTweets.add(tweetId);
try {
// Scroll tweet into view
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(400);
// Click the unlike button
button.click();
stats.unliked++;
// Log progress with age info
if (CONFIG.logProgress && stats.unliked % 5 === 0) {
const elapsed = Math.floor((Date.now() - stats.startTime) / 1000);
console.log(`📅 Progress: ${stats.unliked} unliked | ⏱️ ${formatTime(elapsed)} elapsed`);
} else {
console.log(`💔 Unliked tweet from ${age.days} days ago (${age.date.toLocaleDateString()})`);
}
// Random delay before next action
await sleep(randomDelay());
} catch (error) {
stats.errors++;
console.warn(`⚠️ Error unliking tweet: ${error.message}`);
await sleep(1000);
}
// Check if we've hit the limit
if (stats.unliked >= CONFIG.maxUnlikes) {
console.log(`🎯 Reached maximum unlike limit (${CONFIG.maxUnlikes})`);
break;
}
}
delete window.stopUnlike;
// ═══════════════════════════════════════════════════════════════════════════
// 📊 COMPLETION SUMMARY
// ═══════════════════════════════════════════════════════════════════════════
const totalTime = Math.floor((Date.now() - stats.startTime) / 1000);
const avgTime = stats.unliked > 0 ? (totalTime / stats.unliked).toFixed(2) : 0;
console.log('');
console.log(`
╔══════════════════════════════════════════════════════════════╗
║ ✅ UNLIKE OLD TWEETS COMPLETE ║
╠══════════════════════════════════════════════════════════════╣
║ 📋 Criteria: Tweets older than ${String(CONFIG.daysOld + ' days').padEnd(25)}║
║ 📊 Results: ║
║ 📅 Old tweets unliked: ${String(stats.unliked).padEnd(33)}║
║ ❌ Errors: ${String(stats.errors).padEnd(45)}║
║ ⏱️ Total time: ${formatTime(totalTime).padEnd(40)}║
║ 📈 Avg time/unlike: ${String(avgTime + 's').padEnd(36)}║
╚══════════════════════════════════════════════════════════════╝
`);
console.log('👋 Thanks for using XActions! Follow @nichxbt for updates.');
return stats;
})();
});
register("update-banner", function(){
var CONFIG = {
// Delay for UI interactions (ms)
actionDelay: 1500,
// Auto-open the edit profile modal
autoOpenEditor: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function updateBanner() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $editProfileBtn = '[data-testid="editProfileButton"]';
const $addBannerBtn = '[data-testid="addBannerButton"], [data-testid="editProfileHeader"]';
const $bannerImageInput = 'input[data-testid="fileInput"]';
const $saveButton = '[data-testid="Profile_Save_Button"]';
const $cancelBtn = '[data-testid="cancelButton"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🏞️ UPDATE BANNER IMAGE ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Check if edit profile modal is open
let bannerBtn = document.querySelector($addBannerBtn);
if (!bannerBtn) {
// Try to click edit profile button
const editBtn = document.querySelector($editProfileBtn);
if (editBtn) {
console.log('📍 Opening profile editor...');
editBtn.click();
await sleep(CONFIG.actionDelay * 2);
bannerBtn = document.querySelector($addBannerBtn);
} else {
console.error('❌ ERROR: Edit profile button not found!');
console.log('');
console.log('📋 Please go to your profile page:');
console.log(' https://x.com/YOUR_USERNAME');
return;
}
}
console.log('🖼️ Profile editor opened!');
console.log('');
// Create helper functions
window.XActions = window.XActions || {};
window.XActions.Banner = {
// Click the banner area to trigger file selection
selectFile: () => {
const bannerBtn = document.querySelector($addBannerBtn);
if (bannerBtn) {
bannerBtn.click();
console.log('📂 File picker should open. Select your banner image.');
console.log('');
console.log('💡 Recommended size: 1500 x 500 pixels (3:1 ratio)');
} else {
console.error('❌ Banner button not found. Make sure the profile editor is open.');
}
},
// Save changes
save: async () => {
const saveBtn = document.querySelector($saveButton);
if (saveBtn) {
saveBtn.click();
await sleep(CONFIG.actionDelay);
console.log('✅ Profile saved!');
} else {
console.error('❌ Save button not found.');
}
},
// Cancel changes
cancel: () => {
const cancelBtn = document.querySelector($cancelBtn);
if (cancelBtn) {
cancelBtn.click();
console.log('❌ Changes cancelled.');
}
},
// Help
help: () => {
console.log('');
console.log('📋 AVAILABLE COMMANDS:');
console.log('');
console.log(' XActions.Banner.selectFile()');
console.log(' → Opens file picker to select new banner');
console.log('');
console.log(' XActions.Banner.save()');
console.log(' → Saves the profile changes');
console.log('');
console.log(' XActions.Banner.cancel()');
console.log(' → Cancels changes and closes editor');
console.log('');
console.log('📐 BANNER DIMENSIONS:');
console.log(' Recommended: 1500 x 500 pixels (3:1 ratio)');
console.log(' Minimum: 600 x 200 pixels');
console.log(' Max file size: 2 MB');
console.log('');
}
};
console.log('✅ Banner Helper loaded!');
console.log('');
console.log('📋 INSTRUCTIONS:');
console.log('');
console.log(' Step 1: Run XActions.Banner.selectFile()');
console.log(' Or click on the banner area directly');
console.log(' Step 2: Choose your image file (1500x500 recommended)');
console.log(' Step 3: Adjust the crop if needed');
console.log(' Step 4: Click Apply, then run XActions.Banner.save()');
console.log('');
console.log('💡 Type XActions.Banner.help() for more info');
console.log('');
// Auto-click banner area if enabled
if (CONFIG.autoOpenEditor && bannerBtn) {
console.log('👆 Click on the banner area at the top to change it.');
}
})();
});
register("update-bio", function(){
var CONFIG = {
// Your new bio text (max 160 characters)
newBio: `🚀 Building cool stuff with code
🐦 Automating X with @XActions
💡 Open source enthusiast
🔗 github.com/nirholas/XActions`,
// Delay for UI interactions (ms)
actionDelay: 1000,
// Auto-save after updating
autoSave: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function updateBio() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Set an input's value through the native setter so React's value tracker
// registers the change (direct .value assignment gets ignored by React and
// the old bio would be saved).
const setNativeValue = (el, value) => {
const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value');
if (desc && desc.set) desc.set.call(el, value);
else el.value = value;
};
// DOM Selectors
const $bioTextarea = 'textarea[name="description"]';
const $bioInput = '[data-testid="ocf-bio-input"]';
const $saveButton = '[data-testid="Profile_Save_Button"]';
const $editProfileBtn = '[data-testid="editProfileButton"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✏️ UPDATE PROFILE BIO ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Validate bio length
if (CONFIG.newBio.length > 160) {
console.error('❌ ERROR: Bio exceeds 160 characters!');
console.log(` Current length: ${CONFIG.newBio.length} characters`);
console.log(' Please shorten your bio.');
return;
}
console.log(`📝 New bio (${CONFIG.newBio.length}/160 chars):`);
console.log(` "${CONFIG.newBio}"`);
console.log('');
// Check if we're on profile settings page
const isSettingsPage = window.location.href.includes('/settings/profile');
// Check if edit profile modal is open
let bioField = document.querySelector($bioTextarea) || document.querySelector($bioInput);
if (!bioField && !isSettingsPage) {
// Try to click edit profile button
const editBtn = document.querySelector($editProfileBtn);
if (editBtn) {
console.log('📍 Opening profile editor...');
editBtn.click();
await sleep(CONFIG.actionDelay * 2);
bioField = document.querySelector($bioTextarea) || document.querySelector($bioInput);
}
}
// The settings page (or the modal we just opened) may still be mounting
// its form on first paint. Poll briefly before giving up instead of
// failing immediately on a field that would appear a moment later.
let waitAttempts = 0;
while (!bioField && waitAttempts < 3) {
await sleep(CONFIG.actionDelay);
bioField = document.querySelector($bioTextarea) || document.querySelector($bioInput);
waitAttempts++;
}
if (!bioField) {
console.error('❌ ERROR: Bio field not found!');
console.log('');
console.log('📋 Please try one of these:');
console.log(' 1. Go to: https://x.com/settings/profile');
console.log(' 2. Or click "Edit profile" on your profile page first');
return;
}
// Clear and set new bio
console.log('🔄 Updating bio...');
// Focus the field
bioField.focus();
await sleep(300);
// Select all and delete
bioField.select ? bioField.select() : null;
document.execCommand('selectAll', false, null);
await sleep(100);
// Clear current content
setNativeValue(bioField, '');
bioField.textContent = '';
bioField.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(300);
// Type new bio
setNativeValue(bioField, CONFIG.newBio);
bioField.textContent = CONFIG.newBio;
bioField.dispatchEvent(new Event('input', { bubbles: true }));
bioField.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(CONFIG.actionDelay);
console.log('✅ Bio field updated!');
// Auto-save if enabled
if (CONFIG.autoSave) {
const saveBtn = document.querySelector($saveButton);
if (saveBtn) {
console.log('💾 Saving profile...');
saveBtn.click();
await sleep(CONFIG.actionDelay * 2);
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 PROFILE BIO UPDATED SUCCESSFULLY! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
} else {
console.log('⚠️ Save button not found. Please save manually.');
}
} else {
console.log('');
console.log('💡 Bio field updated. Click "Save" to apply changes.');
}
})();
});
register("update-profile-picture", function(){
var CONFIG = {
// Delay for UI interactions (ms)
actionDelay: 1500,
// Auto-open the file picker
autoOpenPicker: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(async function updateProfilePicture() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// DOM Selectors
const $editProfileBtn = '[data-testid="editProfileButton"]';
const $avatarImageInput = 'input[data-testid="fileInput"][accept*="image"]';
const $addAvatarBtn = '[data-testid="addAvatarButton"], [data-testid="editProfileAvatar"]';
const $avatarContainer = '[data-testid="UserAvatar-Container"]';
const $saveButton = '[data-testid="Profile_Save_Button"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🖼️ UPDATE PROFILE PICTURE ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Check if edit profile modal is open
let avatarBtn = document.querySelector($addAvatarBtn);
let fileInput = document.querySelector($avatarImageInput);
if (!avatarBtn && !fileInput) {
// Try to click edit profile button
const editBtn = document.querySelector($editProfileBtn);
if (editBtn) {
console.log('📍 Opening profile editor...');
editBtn.click();
await sleep(CONFIG.actionDelay * 2);
avatarBtn = document.querySelector($addAvatarBtn);
fileInput = document.querySelector($avatarImageInput);
} else {
console.error('❌ ERROR: Edit profile button not found!');
console.log('');
console.log('📋 Please go to your profile page:');
console.log(' https://x.com/YOUR_USERNAME');
return;
}
}
console.log('📸 Profile editor opened!');
console.log('');
// Create helper functions
window.XActions = window.XActions || {};
window.XActions.ProfilePicture = {
// Trigger file picker
selectFile: () => {
const input = document.querySelector($avatarImageInput);
if (input) {
input.click();
console.log('📂 File picker opened. Select your new profile picture.');
} else {
// Click on avatar to trigger picker
const avatarBtn = document.querySelector($addAvatarBtn);
if (avatarBtn) {
avatarBtn.click();
console.log('📂 Clicked avatar button. Select your new profile picture.');
} else {
console.error('❌ Could not find file input or avatar button.');
}
}
},
// Save changes
save: async () => {
const saveBtn = document.querySelector($saveButton);
if (saveBtn) {
saveBtn.click();
await sleep(CONFIG.actionDelay);
console.log('✅ Profile saved!');
} else {
console.error('❌ Save button not found.');
}
},
// Help
help: () => {
console.log('');
console.log('📋 AVAILABLE COMMANDS:');
console.log('');
console.log(' XActions.ProfilePicture.selectFile()');
console.log(' → Opens file picker to select new profile picture');
console.log('');
console.log(' XActions.ProfilePicture.save()');
console.log(' → Saves the profile changes');
console.log('');
}
};
console.log('✅ Profile Picture Helper loaded!');
console.log('');
console.log('📋 INSTRUCTIONS:');
console.log('');
console.log(' Step 1: Run XActions.ProfilePicture.selectFile()');
console.log(' Step 2: Choose your image file');
console.log(' Step 3: Crop/adjust the image');
console.log(' Step 4: Run XActions.ProfilePicture.save()');
console.log('');
console.log('💡 Type XActions.ProfilePicture.help() for more info');
console.log('');
// Auto-open file picker if enabled
if (CONFIG.autoOpenPicker) {
await sleep(CONFIG.actionDelay);
console.log('🔄 Auto-opening file picker...');
const avatarBtn = document.querySelector($addAvatarBtn);
const avatarContainer = document.querySelector($avatarContainer);
if (avatarBtn) {
avatarBtn.click();
console.log('📂 Click on your avatar to select a new picture!');
} else if (avatarContainer) {
avatarContainer.click();
console.log('📂 Click on your avatar area to select a new picture!');
}
}
})();
});
register("video-downloader", function(){
var CONFIG = {
// Quality preference: 'highest', 'lowest', 'all'
quality: 'highest',
// Auto-download best quality
autoDownload: true,
// Show all available qualities in console
showAllQualities: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function videoDownloader() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🎬 VIDEO DOWNLOADER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Verify we're on a tweet page
if (!window.location.href.includes('/status/')) {
console.error('❌ ERROR: Must be on a tweet page with a video!');
console.log('📍 Go to any tweet: https://x.com/user/status/TWEET_ID');
return;
}
console.log('🔍 Searching for video URLs...');
console.log('');
const videoUrls = new Set();
// Method 1: Check video elements directly
document.querySelectorAll('video').forEach(video => {
if (video.src && video.src.includes('video.twimg.com')) {
videoUrls.add(video.src);
}
// Check source elements
video.querySelectorAll('source').forEach(source => {
if (source.src && source.src.includes('video.twimg.com')) {
videoUrls.add(source.src);
}
});
});
// Method 2: Search in page source for video URLs
const pageHtml = document.documentElement.innerHTML;
const videoRegex = /https:\/\/video\.twimg\.com\/[^"'\s]+\.mp4[^"'\s]*/g;
const matches = pageHtml.match(videoRegex);
if (matches) {
matches.forEach(url => {
// Clean up the URL
const cleanUrl = url.split('"')[0].split("'")[0].split('\\')[0];
if (cleanUrl.includes('.mp4')) {
videoUrls.add(cleanUrl);
}
});
}
// Method 3: Check for blob URLs and try to find actual source
document.querySelectorAll('video[src^="blob:"]').forEach(video => {
console.log('📍 Found blob video - checking for source...');
// For blob videos, we need to check network requests
});
if (videoUrls.size === 0) {
console.log('❌ No video URLs found!');
console.log('');
console.log('💡 Tips:');
console.log(' - Make sure the video has loaded/played');
console.log(' - Try refreshing the page');
console.log(' - Some videos may use DRM protection');
return;
}
// Parse and sort by quality
const videos = [];
videoUrls.forEach(url => {
// Try to extract resolution from URL
const resMatch = url.match(/\/(\d{3,4})x(\d{3,4})\//);
let width = 0, height = 0;
if (resMatch) {
width = parseInt(resMatch[1]);
height = parseInt(resMatch[2]);
} else {
// Try alternate pattern
const altMatch = url.match(/vid\/(\d+)x(\d+)\//);
if (altMatch) {
width = parseInt(altMatch[1]);
height = parseInt(altMatch[2]);
}
}
videos.push({
url: url,
width,
height,
resolution: width && height ? `${width}x${height}` : 'Unknown',
pixels: width * height
});
});
// Sort by resolution (highest first)
videos.sort((a, b) => b.pixels - a.pixels);
// Display results
console.log(`✅ Found ${videos.length} video URL(s):`);
console.log('');
if (CONFIG.showAllQualities) {
console.log('📊 Available qualities:');
videos.forEach((v, i) => {
console.log(` ${i + 1}. ${v.resolution}`);
console.log(` ${v.url}`);
console.log('');
});
}
// Download a single video (fetch as blob so the saved file gets a real
// filename instead of whatever video.twimg.com's URL path ends in).
async function downloadVideo(video, filename) {
try {
const response = await fetch(video.url);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
return true;
} catch (e) {
console.log(`⚠️ Auto-download failed for ${video.resolution}. Opening in new tab...`);
window.open(video.url, '_blank');
return false;
}
}
// 'all' downloads every quality variant found, not just one
if (CONFIG.quality === 'all') {
console.log(`🎯 Quality set to "all": downloading all ${videos.length} variant(s)`);
console.log('');
if (CONFIG.autoDownload) {
for (let i = 0; i < videos.length; i++) {
const v = videos[i];
console.log(`💾 Downloading ${i + 1}/${videos.length} (${v.resolution})...`);
await downloadVideo(v, `twitter_video_${Date.now()}_${v.resolution}.mp4`);
// Small delay so the browser isn't asked to start N simultaneous
// blob downloads at once.
if (i < videos.length - 1) await sleep(800);
}
console.log('✅ All downloads started!');
}
try {
await navigator.clipboard.writeText(videos[0].url);
console.log('📋 Highest quality URL copied to clipboard!');
} catch (e) {}
console.log('');
console.log('💡 You can also right-click the video URL above and "Open in new tab"');
console.log(' then right-click the video and "Save video as..."');
console.log('');
window.videoUrls = videos;
console.log('💡 Access all URLs via: window.videoUrls');
return videos;
}
// Get preferred video
let selectedVideo;
if (CONFIG.quality === 'highest') {
selectedVideo = videos[0];
} else if (CONFIG.quality === 'lowest') {
selectedVideo = videos[videos.length - 1];
} else {
console.warn(`⚠️ Unknown CONFIG.quality "${CONFIG.quality}", defaulting to "highest".`);
selectedVideo = videos[0];
}
if (selectedVideo) {
console.log(`🎯 Selected: ${selectedVideo.resolution}`);
console.log('');
// Copy URL to clipboard
try {
await navigator.clipboard.writeText(selectedVideo.url);
console.log('📋 Video URL copied to clipboard!');
} catch (e) {}
// Auto-download
if (CONFIG.autoDownload) {
console.log('💾 Starting download...');
console.log('');
try {
const response = await fetch(selectedVideo.url);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `twitter_video_${Date.now()}.mp4`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('✅ Download started!');
} catch (e) {
console.log('⚠️ Auto-download failed. Opening in new tab...');
window.open(selectedVideo.url, '_blank');
}
}
}
console.log('');
console.log('💡 You can also right-click the video URL above and "Open in new tab"');
console.log(' then right-click the video and "Save video as..."');
console.log('');
window.videoUrls = videos;
console.log('💡 Access all URLs via: window.videoUrls');
return videos;
})();
});
register("viral-tweets-scraper", function(){
var CONFIG = {
// ---- MINIMUM THRESHOLDS ----
// Only include tweets with at least this many:
minLikes: 50,
minRetweets: 5,
minReplies: 0,
// ---- LIMITS ----
maxTweets: 100,
maxScrolls: 50,
// ---- SORTING ----
// Options: 'likes', 'retweets', 'replies', 'engagement' (sum of all)
sortBy: 'likes',
// ---- TIMING ----
scrollDelay: 1500,
maxRetries: 5,
// ---- EXPORT ----
exportJSON: true,
exportCSV: true
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START
* ============================================================
*/
(async function viralTweetsScraper() {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const $tweet = 'article[data-testid="tweet"]';
const $tweetText = '[data-testid="tweetText"]';
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 🔥 VIRAL TWEETS SCRAPER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
console.log(`🎯 Min thresholds: ${CONFIG.minLikes} likes, ${CONFIG.minRetweets} RTs`);
console.log(`📊 Sort by: ${CONFIG.sortBy}`);
console.log(`📋 Max tweets: ${CONFIG.maxTweets}`);
console.log('');
const tweets = [];
const seenIds = new Set();
let scrolls = 0;
let retries = 0;
let lastCount = 0;
/**
* Parse engagement numbers (handles K, M suffixes)
*/
function parseNumber(str) {
if (!str || str === '') return 0;
str = str.trim().toUpperCase();
if (str.includes('K')) return parseFloat(str) * 1000;
if (str.includes('M')) return parseFloat(str) * 1000000;
if (str.includes('B')) return parseFloat(str) * 1000000000;
return parseInt(str.replace(/,/g, '')) || 0;
}
/**
* Extract tweet data
*/
function extractTweet(tweetEl) {
try {
// Get ID from the timestamp's permalink anchor; the first /status/
// link in the article can belong to a quoted tweet
const timeAnchor = tweetEl.querySelector('time')?.closest('a[href*="/status/"]');
const link = timeAnchor || tweetEl.querySelector('a[href*="/status/"]');
if (!link) return null;
const match = link.href.match(/\/status\/(\d+)/);
if (!match) return null;
const tweetId = match[1];
if (seenIds.has(tweetId)) return null;
seenIds.add(tweetId);
// Get author (User-Name block, not the first profile link, which is
// the reposter on retweets)
const authorLink = tweetEl.querySelector('div[data-testid="User-Name"] a[href^="/"]') ||
tweetEl.querySelector('a[href^="/"][role="link"]');
const username = authorLink ? authorLink.getAttribute('href').replace('/', '').split('/')[0] : 'unknown';
// Get text
const textEl = tweetEl.querySelector($tweetText);
const text = textEl ? textEl.innerText : '';
// Get timestamp
const timeEl = tweetEl.querySelector('time');
const timestamp = timeEl ? timeEl.getAttribute('datetime') : null;
const displayTime = timeEl ? timeEl.innerText : '';
// Get metrics
const getMetric = (testId) => {
const el = tweetEl.querySelector(`[data-testid="${testId}"]`);
const span = el?.querySelector('span span');
return span ? span.innerText : '0';
};
const likesStr = getMetric('like');
const retweetsStr = getMetric('retweet');
const repliesStr = getMetric('reply');
const likes = parseNumber(likesStr);
const retweets = parseNumber(retweetsStr);
const replies = parseNumber(repliesStr);
// Check thresholds
if (likes < CONFIG.minLikes) return null;
if (retweets < CONFIG.minRetweets) return null;
if (replies < CONFIG.minReplies) return null;
// Check for media
const hasImage = tweetEl.querySelector('[data-testid="tweetPhoto"]') !== null;
const hasVideo = tweetEl.querySelector('[data-testid="videoPlayer"], [data-testid="videoComponent"]') !== null;
return {
id: tweetId,
url: `https://x.com/${username}/status/${tweetId}`,
author: username,
text,
timestamp,
displayTime,
metrics: {
likes,
retweets,
replies,
engagement: likes + retweets + replies,
likesDisplay: likesStr,
retweetsDisplay: retweetsStr,
repliesDisplay: repliesStr
},
hasImage,
hasVideo
};
} catch (e) {
return null;
}
}
console.log('🚀 Scraping tweets...');
console.log('');
// Scroll and extract
while (tweets.length < CONFIG.maxTweets && scrolls < CONFIG.maxScrolls && retries < CONFIG.maxRetries) {
document.querySelectorAll($tweet).forEach(el => {
const tweet = extractTweet(el);
if (tweet) tweets.push(tweet);
});
// Stall detection tracks all tweets seen, not just qualifying ones:
// long stretches below the viral thresholds must not end the scan early
if (seenIds.size === lastCount) {
retries++;
} else {
retries = 0;
lastCount = seenIds.size;
}
console.log(`📊 Found ${tweets.length} viral tweets...`);
window.scrollTo(0, document.body.scrollHeight);
await sleep(CONFIG.scrollDelay);
scrolls++;
}
// Sort
const sortKey = CONFIG.sortBy === 'engagement' ? 'engagement' : CONFIG.sortBy;
tweets.sort((a, b) => {
if (sortKey === 'engagement') {
return b.metrics.engagement - a.metrics.engagement;
}
return b.metrics[sortKey] - a.metrics[sortKey];
});
console.log('');
console.log(`✅ Found ${tweets.length} viral tweets`);
console.log('');
// Display top 10
console.log('🏆 TOP 10 VIRAL TWEETS:');
console.log('');
tweets.slice(0, 10).forEach((t, i) => {
console.log(`${i + 1}. [${t.metrics.likesDisplay} ❤️ | ${t.metrics.retweetsDisplay} 🔄] @${t.author}`);
console.log(` "${t.text.substring(0, 60)}..."`);
console.log(` ${t.url}`);
console.log('');
});
// Build result
const result = {
scrapedAt: new Date().toISOString(),
sortedBy: CONFIG.sortBy,
totalTweets: tweets.length,
tweets
};
const dateStr = new Date().toISOString().split('T')[0];
// Download JSON
if (CONFIG.exportJSON) {
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `viral_tweets_${dateStr}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('💾 JSON downloaded!');
}
// Download CSV
if (CONFIG.exportCSV) {
const headers = ['Rank', 'Author', 'Likes', 'Retweets', 'Replies', 'Engagement', 'Text', 'Date', 'Has Image', 'Has Video', 'URL'];
const rows = tweets.map((t, i) => [
i + 1,
t.author,
t.metrics.likes,
t.metrics.retweets,
t.metrics.replies,
t.metrics.engagement,
`"${t.text.substring(0, 200).replace(/"/g, '""').replace(/\n/g, ' ')}"`,
`"${(t.displayTime || '').replace(/"/g, '""')}"`,
t.hasImage,
t.hasVideo,
t.url
].join(','));
const csv = [headers.join(','), ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `viral_tweets_${dateStr}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('💾 CSV downloaded!');
}
console.log('');
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ VIRAL TWEETS SCRAPER COMPLETE! ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log(`🔥 Total viral tweets: ${tweets.length}`);
console.log('');
window.viralTweets = result;
console.log('💡 Access via: window.viralTweets');
return result;
})();
});
register("vote-in-polls", function(){
(async function voteInPolls() {
'use strict';
// ============================================
// 📝 CONFIGURATION - Customize these options
// ============================================
const CONFIG = {
// How to choose an option in each poll:
// 'first' = the first option
// 'last' = the last option
// 'random' = a random option
// 'index' = the option at CONFIG.optionIndex (0-based)
strategy: 'random',
// Used only when strategy is 'index'. 0 = first option.
optionIndex: 0,
// Maximum number of polls to vote in this run
maxVotes: 15,
// Minimum delay between votes (ms)
minDelay: 1500,
// Maximum delay between votes (ms)
maxDelay: 4000,
// Maximum scroll attempts to find more polls
maxScrollAttempts: 30,
// Stop if no new polls appear after this many consecutive scrolls
noNewPollsThreshold: 6
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
// ============================================
// 🔧 SELECTORS
// ============================================
const SELECTORS = {
tweet: 'article[data-testid="tweet"]',
tweetText: '[data-testid="tweetText"]',
// X renders poll options with a pollChoice testid; some builds expose them
// as ARIA radios inside the card. We match both, then keep only clickable ones.
pollChoice: '[data-testid="pollChoice"], [data-testid^="pollChoice"], div[role="radio"]'
};
// ============================================
// 🛠️ HELPERS
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = () => {
const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
return sleep(delay);
};
const scrollDown = () => window.scrollBy(0, window.innerHeight * 0.7);
const log = {
info: (msg) => console.log(`ℹ️ ${msg}`),
success: (msg) => console.log(`✅ ${msg}`),
warning: (msg) => console.log(`⚠️ ${msg}`),
error: (msg) => console.log(`❌ ${msg}`),
progress: (current, total) => console.log(`📊 Progress: ${current}/${total} voted`)
};
const getTweetId = (tweet) => {
const timeAnchor = tweet.querySelector('time')?.closest('a[href*="/status/"]');
if (timeAnchor) {
const match = timeAnchor.href.match(/\/status\/(\d+)/);
if (match) return match[1];
}
const link = tweet.querySelector('a[href*="/status/"]');
const m = link?.getAttribute('href')?.match(/\/status\/(\d+)/);
return m ? m[1] : (tweet.querySelector(SELECTORS.tweetText)?.textContent || '').substring(0, 100);
};
// Return the clickable poll option elements in a post, or [] if there is no
// open, unvoted poll. A poll already voted or closed renders results (percent
// bars) instead of interactive choices, so those elements are filtered out.
const getOpenPollChoices = (tweet) => {
const raw = Array.from(tweet.querySelectorAll(SELECTORS.pollChoice));
if (raw.length === 0) return [];
// Deduplicate: pollChoice and role="radio" can select nested duplicates.
const seen = new Set();
const choices = [];
for (const el of raw) {
// Prefer the interactive ancestor (the actual clickable option row).
const clickable = el.closest('[role="radio"], button, [role="button"]') || el;
if (seen.has(clickable)) continue;
seen.add(clickable);
// Skip options that are disabled or already show as selected/results.
const ariaDisabled = clickable.getAttribute('aria-disabled') === 'true';
const disabledAttr = clickable.hasAttribute('disabled');
if (ariaDisabled || disabledAttr) continue;
choices.push(clickable);
}
// If any choice shows a result percentage, the poll is voted/closed -> skip.
const showsResults = /\d+(\.\d+)?%/.test(tweet.textContent || '');
if (showsResults && choices.every(c => c.getAttribute('role') !== 'radio')) {
return [];
}
// A genuine open poll has 2+ selectable options.
return choices.length >= 2 ? choices : [];
};
const chooseIndex = (count) => {
switch (CONFIG.strategy) {
case 'first': return 0;
case 'last': return count - 1;
case 'random': return Math.floor(Math.random() * count);
case 'index': return Math.min(Math.max(CONFIG.optionIndex, 0), count - 1);
default: return 0;
}
};
// ============================================
// 🎯 MAIN LOGIC
// ============================================
const stats = {
voted: 0,
pollsSeen: 0,
skippedClosedOrVoted: 0,
errors: 0
};
const processed = new Set();
// Stop switch: run window.stopVoteInPolls() to abort after the current poll.
let stopped = false;
window.stopVoteInPolls = () => {
stopped = true;
log.warning('Stop requested. Finishing the current poll, then exiting.');
};
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 🗳️ VOTE IN POLLS - XActions ║
║ 👤 Author: nichxbt ║
║ 🌐 https://xactions.app ║
╚══════════════════════════════════════════════════════════╝
`);
// Page guard: needs a rendered feed. Warn, do not redirect.
if (!/x\.com|twitter\.com/.test(window.location.href)) {
log.warning('Open x.com on a timeline or search page, then run this again.');
return;
}
if (document.querySelector(SELECTORS.tweet) === null) {
log.warning('No posts visible yet. Go to a timeline or search results page, wait for it to load, then re-run.');
return;
}
const validStrategies = ['first', 'last', 'random', 'index'];
if (!validStrategies.includes(CONFIG.strategy)) {
log.warning(`Unknown strategy "${CONFIG.strategy}". Falling back to 'first'.`);
CONFIG.strategy = 'first';
}
log.info(`Strategy: ${CONFIG.strategy}${CONFIG.strategy === 'index' ? ` (option ${CONFIG.optionIndex})` : ''} | Max votes: ${CONFIG.maxVotes}`);
log.info('To stop early: window.stopVoteInPolls()');
let scrollAttempts = 0;
let noNewPollsCount = 0;
while (!stopped && stats.voted < CONFIG.maxVotes && scrollAttempts < CONFIG.maxScrollAttempts) {
const tweets = document.querySelectorAll(SELECTORS.tweet);
let foundNewPoll = false;
for (const tweet of tweets) {
if (stopped || stats.voted >= CONFIG.maxVotes) break;
const tweetId = getTweetId(tweet);
if (processed.has(tweetId)) continue;
// Only mark posts that actually contain a poll as processed, so we do not
// burn the dedupe set on the majority of non-poll posts before they render.
const rawHasChoice = tweet.querySelector(SELECTORS.pollChoice) !== null;
if (!rawHasChoice) continue;
processed.add(tweetId);
stats.pollsSeen++;
foundNewPoll = true;
try {
const choices = getOpenPollChoices(tweet);
if (choices.length === 0) {
stats.skippedClosedOrVoted++;
continue;
}
const idx = chooseIndex(choices.length);
const target = choices[idx];
const label = (target.textContent || '').substring(0, 40).replace(/\n/g, ' ').trim() || `option ${idx + 1}`;
tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
target.click();
await sleep(700);
// Verify the vote registered: the poll should now show results.
const registered = /\d+(\.\d+)?%/.test(tweet.textContent || '') ||
tweet.querySelector(SELECTORS.pollChoice) === null ||
getOpenPollChoices(tweet).length === 0;
if (registered) {
stats.voted++;
log.success(`Voted #${stats.voted}: "${label}" (${CONFIG.strategy})`);
log.progress(stats.voted, CONFIG.maxVotes);
await randomDelay();
} else {
log.warning('Click did not register a vote (poll may have closed). Skipping.');
stats.skippedClosedOrVoted++;
}
} catch (error) {
log.error(`Error voting in poll: ${error.message}`);
stats.errors++;
}
}
if (!foundNewPoll) {
noNewPollsCount++;
if (noNewPollsCount >= CONFIG.noNewPollsThreshold) {
log.warning('No new polls found after multiple scrolls. Stopping.');
break;
}
} else {
noNewPollsCount = 0;
}
scrollDown();
scrollAttempts++;
log.info(`Scrolling for more polls... (attempt ${scrollAttempts}/${CONFIG.maxScrollAttempts})`);
await sleep(1500);
}
// ============================================
// 📊 SUMMARY
// ============================================
console.log(`
╔══════════════════════════════════════════════════════════╗
║ 📊 VOTE IN POLLS - COMPLETE ║
╠══════════════════════════════════════════════════════════╣
║ 🗳️ Voted: ${String(stats.voted).padEnd(28)}║
║ 🔎 Polls Seen: ${String(stats.pollsSeen).padEnd(28)}║
║ ⏭️ Closed/Already Voted: ${String(stats.skippedClosedOrVoted).padEnd(28)}║
║ ❌ Errors: ${String(stats.errors).padEnd(28)}║
║ 📜 Scroll Attempts: ${String(scrollAttempts).padEnd(28)}║
╚══════════════════════════════════════════════════════════╝
`);
if (stats.pollsSeen === 0) {
log.warning('No polls found in this feed. Try a search like x.com/search?q=poll&f=live.');
}
log.success('Script completed! by nichxbt');
return stats;
})();
});
register("whitelist", function(){
var CONFIG = {
// Storage key
storageKey: 'xactions_whitelist',
// Pre-populate with important accounts
defaultWhitelist: [
// 'nichxbt',
// 'importantfriend',
],
};
try{if(typeof window!=='undefined'&&window.__XA_LAUNCH_CFG){Object.assign(CONFIG,window.__XA_LAUNCH_CFG);}}catch(_xa){}
/**
* ============================================================
* 🚀 SCRIPT START - by nichxbt
* ============================================================
*/
(function whitelistManager() {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ ✅ WHITELIST MANAGER ║');
console.log('║ by nichxbt - https://github.com/nirholas/XActions ║');
console.log('╚════════════════════════════════════════════════════════════╝');
console.log('');
// Storage helpers
const getWhitelist = () => {
try {
const data = localStorage.getItem(CONFIG.storageKey);
return data ? JSON.parse(data) : [];
} catch {
return [];
}
};
const saveWhitelist = (list) => {
localStorage.setItem(CONFIG.storageKey, JSON.stringify(list));
};
// Initialize with defaults if empty
const init = () => {
const current = getWhitelist();
if (current.length === 0 && CONFIG.defaultWhitelist.length > 0) {
const defaults = CONFIG.defaultWhitelist.map(u => ({
username: u.replace('@', '').toLowerCase(),
addedAt: Date.now(),
reason: 'default'
}));
saveWhitelist(defaults);
}
};
init();
// Create XActions interface
window.XActions = window.XActions || {};
window.XActions.Whitelist = {
// Add user to whitelist
add: (username, reason = '') => {
const list = getWhitelist();
const clean = username.replace('@', '').toLowerCase();
if (list.find(u => u.username === clean)) {
console.log(`⚠️ @${clean} is already whitelisted.`);
return false;
}
list.push({
username: clean,
addedAt: Date.now(),
reason: reason || 'manual'
});
saveWhitelist(list);
console.log(`✅ Added @${clean} to whitelist.`);
return true;
},
// Add multiple users
addBulk: (usernames) => {
let added = 0;
usernames.forEach(u => {
if (window.XActions.Whitelist.add(u, 'bulk')) added++;
});
console.log(`✅ Added ${added} users to whitelist.`);
},
// Remove user from whitelist
remove: (username) => {
let list = getWhitelist();
const clean = username.replace('@', '').toLowerCase();
const before = list.length;
list = list.filter(u => u.username !== clean);
if (list.length < before) {
saveWhitelist(list);
console.log(`✅ Removed @${clean} from whitelist.`);
return true;
}
console.log(`⚠️ @${clean} was not in whitelist.`);
return false;
},
// Check if user is whitelisted
includes: (username) => {
const list = getWhitelist();
const clean = username.replace('@', '').toLowerCase();
return list.some(u => u.username === clean);
},
// Alias for includes
has: (username) => window.XActions.Whitelist.includes(username),
// Get all whitelisted users
getAll: () => {
return getWhitelist();
},
// Get just usernames
getUsernames: () => {
return getWhitelist().map(u => u.username);
},
// Count
count: () => {
return getWhitelist().length;
},
// List all
list: () => {
const list = getWhitelist();
console.log('');
console.log('═'.repeat(50));
console.log('✅ WHITELISTED USERS');
console.log('═'.repeat(50));
if (list.length === 0) {
console.log('No users whitelisted yet.');
} else {
list.forEach((u, i) => {
const date = new Date(u.addedAt).toLocaleDateString();
console.log(`${i + 1}. @${u.username} (added: ${date}${u.reason ? ', ' + u.reason : ''})`);
});
}
console.log('═'.repeat(50));
console.log(`Total: ${list.length} users`);
console.log('');
},
// Clear all
clear: () => {
if (confirm('⚠️ Clear entire whitelist?')) {
saveWhitelist([]);
console.log('✅ Whitelist cleared.');
}
},
// Export
export: () => {
const list = getWhitelist();
const usernames = list.map(u => u.username);
console.log('📋 Whitelist (copy this):');
console.log(JSON.stringify(usernames));
if (navigator.clipboard) {
navigator.clipboard.writeText(JSON.stringify(usernames)).then(
() => console.log('✅ Copied to clipboard!'),
() => console.log('⚠️ Clipboard copy failed. Copy the JSON above manually.')
);
} else {
console.log('⚠️ Clipboard unavailable. Copy the JSON above manually.');
}
return usernames;
},
// Import
import: (usernamesArray) => {
if (!Array.isArray(usernamesArray)) {
try {
usernamesArray = JSON.parse(usernamesArray);
} catch {
console.error('❌ Invalid format. Provide an array of usernames.');
return;
}
}
window.XActions.Whitelist.addBulk(usernamesArray);
},
// Collect from current page (following list, etc.)
collectFromPage: () => {
const userCells = document.querySelectorAll('[data-testid="UserCell"]');
const users = [];
userCells.forEach(cell => {
const link = cell.querySelector('a[href^="/"]');
const username = link?.getAttribute('href')?.replace('/', '');
if (username && !username.includes('/')) {
users.push(username);
}
});
console.log(`📥 Found ${users.length} users on page.`);
if (users.length > 0) {
const add = confirm(`Add ${users.length} users to whitelist?`);
if (add) {
window.XActions.Whitelist.addBulk(users);
}
}
return users;
},
// Help
help: () => {
console.log('');
console.log('📋 WHITELIST COMMANDS:');
console.log('');
console.log(' XActions.Whitelist.add("username")');
console.log(' XActions.Whitelist.add("user", "reason")');
console.log(' XActions.Whitelist.addBulk(["u1", "u2"])');
console.log(' XActions.Whitelist.remove("username")');
console.log(' XActions.Whitelist.has("username")');
console.log(' XActions.Whitelist.list()');
console.log(' XActions.Whitelist.count()');
console.log(' XActions.Whitelist.export()');
console.log(' XActions.Whitelist.import([...])');
console.log(' XActions.Whitelist.collectFromPage()');
console.log(' XActions.Whitelist.clear()');
console.log('');
}
};
console.log(`✅ Whitelist Manager loaded! (${getWhitelist().length} users)`);
console.log(' Run XActions.Whitelist.help() for commands.');
console.log('');
})();
});
const VERSION = '1.0.0';
const PANEL_ID = 'xactions-command-center';
const FAB_ID = 'xactions-command-center-fab';
const LS_KEY = 'xactions_command_center_v1';
const TAG = '[XActions Command Center]';
const DANGER = {
safe: { label: 'Safe', color: '#00ba7c', note: 'Read-only or export. Does not change your account.' },
caution: { label: 'Writes', color: '#ffd400', note: 'Performs actions on your account (likes, follows, posts). Runs at a human pace.' },
destructive: { label: 'Bulk / irreversible', color: '#f4212e', note: 'Bulk changes that cannot be undone in one click (mass unfollow, unlike, block, clear). Read the warning before running.' }
};
// ---- persisted UI state ----------------------------------------------------
const DEFAULT_STATE = { favorites: [], recents: [], pos: { top: 72, left: null }, category: 'all' };
function loadState() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return structuredClone(DEFAULT_STATE);
const saved = JSON.parse(raw);
return { ...structuredClone(DEFAULT_STATE), ...saved };
} catch (e) { return structuredClone(DEFAULT_STATE); }
}
function saveState() { try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ } }
const state = loadState();
const byId = Object.fromEntries(CATALOG.map((t) => [t.id, t]));
// ---- small helpers ---------------------------------------------------------
function el(tag, attrs, children) {
const node = document.createElement(tag);
if (attrs) {
for (const k of Object.keys(attrs)) {
if (k === 'class') node.className = attrs[k];
else if (k === 'text') node.textContent = attrs[k];
else if (k === 'html') node.innerHTML = attrs[k];
else if (k.startsWith('on') && typeof attrs[k] === 'function') node.addEventListener(k.slice(2), attrs[k]);
else if (attrs[k] !== null && attrs[k] !== undefined) node.setAttribute(k, attrs[k]);
}
}
for (const c of [].concat(children || [])) {
if (c === null || c === undefined) continue;
node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
}
return node;
}
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const clone = (v) => (typeof structuredClone === 'function' ? structuredClone(v) : JSON.parse(JSON.stringify(v)));
function toast(msg, kind) {
const t = el('div', { class: 'xcc-toast ' + (kind || ''), text: msg });
document.body.appendChild(t);
requestAnimationFrame(() => t.classList.add('show'));
setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 250); }, 3200);
}
// ---- styles ----------------------------------------------------------------
const STYLE = `
#${PANEL_ID}, #${FAB_ID} { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; box-sizing: border-box; }
#${PANEL_ID} *, #${FAB_ID} * { box-sizing: border-box; }
#${PANEL_ID} {
position: fixed; width: 380px; max-height: 90vh; background: #15202b; color: #e7e9ea;
border: 1px solid #38444d; border-radius: 16px; font-size: 13px; z-index: 2147483647;
box-shadow: 0 12px 40px rgba(0,0,0,0.55); display: flex; flex-direction: column;
}
#${PANEL_ID} .xcc-header {
background: #1d9bf0; color: #fff; padding: 11px 14px; cursor: move; border-radius: 15px 15px 0 0;
display: flex; align-items: center; justify-content: space-between; font-weight: 800; font-size: 14px; flex: 0 0 auto;
}
#${PANEL_ID} .xcc-header .xcc-hbtns { display: flex; gap: 5px; }
#${PANEL_ID} .xcc-header button {
background: rgba(255,255,255,0.16); border: none; color: #fff; width: 25px; height: 25px;
border-radius: 7px; cursor: pointer; font-size: 13px; line-height: 1; transition: background 0.15s;
}
#${PANEL_ID} .xcc-header button:hover { background: rgba(255,255,255,0.32); }
#${PANEL_ID} .xcc-search { padding: 10px 12px 6px; flex: 0 0 auto; }
#${PANEL_ID} .xcc-search input {
width: 100%; background: #192734; color: #e7e9ea; border: 1px solid #38444d; border-radius: 10px;
padding: 9px 12px; font-size: 13px; outline: none;
}
#${PANEL_ID} .xcc-search input:focus { border-color: #1d9bf0; }
#${PANEL_ID} .xcc-cats { display: flex; gap: 5px; overflow-x: auto; padding: 4px 12px 8px; flex: 0 0 auto; scrollbar-width: none; }
#${PANEL_ID} .xcc-cats::-webkit-scrollbar { display: none; }
#${PANEL_ID} .xcc-cat {
white-space: nowrap; background: #192734; border: 1px solid #38444d; color: #8899a6; border-radius: 999px;
padding: 4px 11px; font-size: 11.5px; font-weight: 700; cursor: pointer; transition: all 0.15s;
}
#${PANEL_ID} .xcc-cat:hover { color: #e7e9ea; }
#${PANEL_ID} .xcc-cat.active { background: #1d9bf0; border-color: #1d9bf0; color: #fff; }
#${PANEL_ID} .xcc-body { overflow-y: auto; flex: 1 1 auto; padding: 0 8px 8px; }
#${PANEL_ID} .xcc-body::-webkit-scrollbar { width: 9px; }
#${PANEL_ID} .xcc-body::-webkit-scrollbar-thumb { background: #38444d; border-radius: 5px; }
#${PANEL_ID} .xcc-group-label { color: #536471; font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.6px; padding: 12px 8px 5px; }
#${PANEL_ID} .xcc-row {
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; cursor: pointer; transition: background 0.12s;
}
#${PANEL_ID} .xcc-row:hover, #${PANEL_ID} .xcc-row.active { background: #1e2a37; }
#${PANEL_ID} .xcc-row .xcc-emoji { font-size: 18px; width: 24px; text-align: center; flex: 0 0 auto; }
#${PANEL_ID} .xcc-row .xcc-meta { flex: 1 1 auto; min-width: 0; }
#${PANEL_ID} .xcc-row .xcc-title { font-weight: 700; display: flex; align-items: center; gap: 7px; }
#${PANEL_ID} .xcc-row .xcc-title .xcc-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; }
#${PANEL_ID} .xcc-row .xcc-sub { color: #8899a6; font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#${PANEL_ID} .xcc-star { flex: 0 0 auto; color: #536471; font-size: 15px; cursor: pointer; padding: 2px; transition: color 0.15s, transform 0.1s; }
#${PANEL_ID} .xcc-star:hover { transform: scale(1.2); }
#${PANEL_ID} .xcc-star.on { color: #ffd400; }
#${PANEL_ID} .xcc-empty { text-align: center; color: #536471; padding: 40px 20px; font-size: 12.5px; }
/* detail */
#${PANEL_ID} .xcc-detail { padding: 10px 14px 4px; }
#${PANEL_ID} .xcc-back { background: none; border: none; color: #1d9bf0; cursor: pointer; font-size: 12.5px; font-weight: 700; padding: 4px 0 8px; display: inline-flex; gap: 5px; align-items: center; }
#${PANEL_ID} .xcc-dtitle { display: flex; align-items: center; gap: 10px; font-size: 17px; font-weight: 800; }
#${PANEL_ID} .xcc-badges { display: flex; gap: 6px; margin: 9px 0; flex-wrap: wrap; }
#${PANEL_ID} .xcc-badge { padding: 3px 9px; border-radius: 999px; font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.4px; }
#${PANEL_ID} .xcc-badge.cat { background: #192734; color: #8899a6; border: 1px solid #38444d; }
#${PANEL_ID} .xcc-ddesc { color: #cfd9e0; font-size: 13px; line-height: 1.5; margin: 4px 0 10px; }
#${PANEL_ID} .xcc-where { background: #192734; border: 1px solid #38444d; border-radius: 10px; padding: 9px 11px; font-size: 12px; margin-bottom: 10px; }
#${PANEL_ID} .xcc-where b { color: #e7e9ea; }
#${PANEL_ID} .xcc-where .xcc-where-link { color: #1d9bf0; text-decoration: none; word-break: break-all; }
#${PANEL_ID} .xcc-where.warn { border-color: #ffd400; }
#${PANEL_ID} .xcc-warn {
background: rgba(244,33,46,0.12); border: 1px solid rgba(244,33,46,0.5); border-radius: 10px;
padding: 9px 11px; font-size: 12px; color: #ffb3b8; margin-bottom: 10px; line-height: 1.45;
}
#${PANEL_ID} .xcc-opts { margin-bottom: 10px; }
#${PANEL_ID} .xcc-opts > summary {
cursor: pointer; font-weight: 800; color: #e7e9ea; list-style: none; padding: 6px 0; font-size: 12.5px;
display: flex; justify-content: space-between; align-items: center;
}
#${PANEL_ID} .xcc-opts > summary::after { content: '\\25be'; color: #8899a6; }
#${PANEL_ID} .xcc-opts[open] > summary::after { content: '\\25b4'; }
#${PANEL_ID} .xcc-optbar { display: flex; gap: 8px; margin: 2px 0 8px; }
#${PANEL_ID} .xcc-optbar button { background: none; border: none; color: #1d9bf0; cursor: pointer; font-size: 11.5px; font-weight: 700; padding: 0; }
#${PANEL_ID} .xcc-field { display: grid; gap: 3px; margin-bottom: 8px; }
#${PANEL_ID} .xcc-field > label { color: #8899a6; font-size: 11px; }
#${PANEL_ID} .xcc-field input[type="text"], #${PANEL_ID} .xcc-field input[type="number"], #${PANEL_ID} .xcc-field select, #${PANEL_ID} .xcc-json {
background: #192734; color: #e7e9ea; border: 1px solid #38444d; border-radius: 8px; padding: 7px 9px; font-size: 12px; width: 100%; outline: none;
}
#${PANEL_ID} .xcc-field input:focus, #${PANEL_ID} .xcc-json:focus { border-color: #1d9bf0; }
#${PANEL_ID} .xcc-check { display: flex; align-items: center; gap: 8px; color: #e7e9ea; font-size: 12.5px; cursor: pointer; margin-bottom: 8px; }
#${PANEL_ID} .xcc-check input { accent-color: #1d9bf0; width: 15px; height: 15px; }
#${PANEL_ID} .xcc-sub-obj { border: 1px solid #2b3742; border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; }
#${PANEL_ID} .xcc-sub-obj > .xcc-sub-title { color: #8899a6; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 6px; }
#${PANEL_ID} .xcc-json { min-height: 90px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
#${PANEL_ID} .xcc-json.bad { border-color: #f4212e; }
#${PANEL_ID} .xcc-run {
width: 100%; border: none; border-radius: 12px; padding: 12px 0; font-weight: 800; font-size: 14px; cursor: pointer;
color: #04120c; background: #00ba7c; transition: filter 0.15s, transform 0.05s; margin-bottom: 4px;
}
#${PANEL_ID} .xcc-run.caution { background: #ffd400; color: #1a1400; }
#${PANEL_ID} .xcc-run.destructive { background: #f4212e; color: #fff; }
#${PANEL_ID} .xcc-run:hover { filter: brightness(1.1); }
#${PANEL_ID} .xcc-run:active { transform: scale(0.99); }
/* dock */
#${PANEL_ID} .xcc-dock { border-top: 1px solid #38444d; padding: 8px 12px; flex: 0 0 auto; max-height: 150px; overflow-y: auto; }
#${PANEL_ID} .xcc-dock-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
#${PANEL_ID} .xcc-dock-head span { color: #536471; font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; }
#${PANEL_ID} .xcc-dock-head button { background: none; border: none; color: #f4212e; font-size: 11px; font-weight: 700; cursor: pointer; padding: 0; }
#${PANEL_ID} .xcc-run-item { display: flex; align-items: center; gap: 8px; background: #192734; border-radius: 8px; padding: 6px 9px; margin-bottom: 5px; font-size: 12px; }
#${PANEL_ID} .xcc-run-item .xcc-ri-meta { flex: 1 1 auto; min-width: 0; }
#${PANEL_ID} .xcc-run-item .xcc-ri-title { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#${PANEL_ID} .xcc-run-item .xcc-ri-time { color: #536471; font-size: 10.5px; }
#${PANEL_ID} .xcc-run-item button { background: #f4212e; border: none; color: #fff; border-radius: 7px; padding: 4px 10px; font-size: 11px; font-weight: 700; cursor: pointer; flex: 0 0 auto; }
#${PANEL_ID} .xcc-run-item button.ghost { background: #38444d; }
#${PANEL_ID} .xcc-footer { padding: 7px 14px 10px; color: #536471; font-size: 10.5px; text-align: center; flex: 0 0 auto; }
#${PANEL_ID} .xcc-footer a { color: #1d9bf0; text-decoration: none; }
#${PANEL_ID}.xcc-min .xcc-search, #${PANEL_ID}.xcc-min .xcc-cats, #${PANEL_ID}.xcc-min .xcc-body, #${PANEL_ID}.xcc-min .xcc-footer, #${PANEL_ID}.xcc-min .xcc-dock { display: none; }
#${FAB_ID} {
position: fixed; bottom: 22px; right: 22px; width: 52px; height: 52px; border-radius: 50%; background: #1d9bf0;
color: #fff; border: none; cursor: pointer; font-size: 24px; z-index: 2147483646; box-shadow: 0 6px 20px rgba(29,155,240,0.5);
display: flex; align-items: center; justify-content: center; transition: transform 0.15s, filter 0.15s;
}
#${FAB_ID}:hover { transform: scale(1.08); filter: brightness(1.08); }
.xcc-toast {
position: fixed; bottom: 22px; left: 50%; transform: translate(-50%, 20px); background: #192734; color: #e7e9ea;
border: 1px solid #38444d; border-radius: 12px; padding: 11px 18px; font-size: 13px; z-index: 2147483647;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; opacity: 0; transition: opacity 0.25s, transform 0.25s;
box-shadow: 0 8px 24px rgba(0,0,0,0.5); max-width: 80vw;
}
.xcc-toast.show { opacity: 1; transform: translate(-50%, 0); }
.xcc-toast.good { border-color: #00ba7c; }
.xcc-toast.bad { border-color: #f4212e; }
`;
// ---- launched-tool tracking (the dock) ------------------------------------
const launched = []; // { id, title, at, stopGlobal }
function fmtTime(ts) {
const d = new Date(ts);
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
// ---- panel construction ----------------------------------------------------
let panel, fab, els = {}, view = 'list', activeIndex = 0, visibleRows = [];
function build() {
const style = el('style', { id: PANEL_ID + '-style', html: STYLE });
document.head.appendChild(style);
panel = el('div', { id: PANEL_ID });
panel.style.top = (state.pos.top || 72) + 'px';
if (state.pos.left !== null && state.pos.left !== undefined) panel.style.left = state.pos.left + 'px';
else panel.style.right = '22px';
const header = el('div', { class: 'xcc-header' }, [
el('span', { html: '⚡ XActions Command Center' }),
el('div', { class: 'xcc-hbtns' }, [
el('button', { title: 'Minimize', text: '–', onclick: () => panel.classList.toggle('xcc-min') }),
el('button', { title: 'Close', text: '✕', onclick: close })
])
]);
els.search = el('input', { type: 'text', placeholder: 'Search ' + CATALOG.length + ' tools…', spellcheck: 'false', autocomplete: 'off', oninput: () => { activeIndex = 0; renderList(); } });
els.searchWrap = el('div', { class: 'xcc-search' }, [els.search]);
els.cats = el('div', { class: 'xcc-cats' });
els.body = el('div', { class: 'xcc-body' });
els.dock = el('div', { class: 'xcc-dock', style: 'display:none' });
els.footer = el('div', { class: 'xcc-footer', html: 'XActions v' + VERSION + ' · <a href="https://github.com/nirholas/XActions" target="_blank" rel="noopener">github.com/nirholas/XActions</a> · <a href="https://xactions.app" target="_blank" rel="noopener">xactions.app</a>' });
panel.append(header, els.searchWrap, els.cats, els.body, els.dock, els.footer);
document.body.appendChild(panel);
fab = el('button', { id: FAB_ID, title: 'XActions Command Center', html: '⚡', onclick: open, style: 'display:none' });
document.body.appendChild(fab);
renderCats();
renderList();
makeDraggable(header);
document.addEventListener('keydown', onKey, true);
els.search.focus();
}
function renderCats() {
els.cats.innerHTML = '';
const all = [{ id: 'all', label: 'All', emoji: '✨' }, { id: 'favorites', label: 'Favorites', emoji: '⭐' }, ...CATEGORIES];
for (const c of all) {
const chip = el('button', { class: 'xcc-cat' + (state.category === c.id ? ' active' : ''), text: c.emoji + ' ' + c.label, onclick: () => { state.category = c.id; saveState(); activeIndex = 0; renderCats(); renderList(); } });
els.cats.appendChild(chip);
}
}
function matches(tool, q) {
if (!q) return true;
const hay = (tool.title + ' ' + tool.desc + ' ' + tool.category + ' ' + tool.id).toLowerCase();
return q.toLowerCase().split(/\s+/).every((w) => hay.includes(w));
}
function renderList() {
view = 'list';
els.searchWrap.style.display = '';
els.cats.style.display = '';
els.body.innerHTML = '';
const q = els.search.value.trim();
let pool = CATALOG.slice();
if (state.category === 'favorites') pool = pool.filter((t) => state.favorites.includes(t.id));
else if (state.category !== 'all') pool = pool.filter((t) => t.category === state.category);
pool = pool.filter((t) => matches(t, q));
visibleRows = [];
if (!pool.length) {
els.body.appendChild(el('div', { class: 'xcc-empty', html: 'No tools match. Try another search or category.' }));
renderDock();
return;
}
// Favorites + Recents shortcuts only on the unfiltered "all" view.
if (state.category === 'all' && !q) {
if (state.favorites.length) appendGroup('⭐ Favorites', state.favorites.map((id) => byId[id]).filter(Boolean));
const recents = state.recents.map((id) => byId[id]).filter(Boolean);
if (recents.length) appendGroup('\u{1f552} Recent', recents);
}
if (state.category === 'all' && !q) {
for (const cat of CATEGORIES) {
const items = pool.filter((t) => t.category === cat.id);
if (items.length) appendGroup(cat.emoji + ' ' + cat.label, items);
}
} else {
appendGroup(null, pool);
}
highlight();
renderDock();
}
function appendGroup(label, items) {
if (label) els.body.appendChild(el('div', { class: 'xcc-group-label', text: label }));
for (const tool of items) {
const idx = visibleRows.length;
const star = el('span', {
class: 'xcc-star' + (state.favorites.includes(tool.id) ? ' on' : ''), title: 'Favorite', html: '★',
onclick: (e) => { e.stopPropagation(); toggleFav(tool.id); }
});
const row = el('div', { class: 'xcc-row', onclick: () => openDetail(tool.id) }, [
el('span', { class: 'xcc-emoji', text: tool.emoji }),
el('div', { class: 'xcc-meta' }, [
el('div', { class: 'xcc-title' }, [
el('span', { class: 'xcc-dot', style: 'background:' + DANGER[tool.danger].color, title: DANGER[tool.danger].label }),
document.createTextNode(tool.title)
]),
el('div', { class: 'xcc-sub', text: tool.desc })
]),
star
]);
row.dataset.idx = idx;
visibleRows.push({ tool, row });
els.body.appendChild(row);
}
}
function highlight() {
visibleRows.forEach((r, i) => r.row.classList.toggle('active', i === activeIndex));
const active = visibleRows[activeIndex];
if (active) active.row.scrollIntoView({ block: 'nearest' });
}
function toggleFav(id) {
const i = state.favorites.indexOf(id);
if (i >= 0) state.favorites.splice(i, 1); else state.favorites.push(id);
saveState();
if (view === 'list') renderList();
}
function pushRecent(id) {
state.recents = [id, ...state.recents.filter((r) => r !== id)].slice(0, 6);
saveState();
}
// ---- detail view -----------------------------------------------------------
function openDetail(id) {
const tool = byId[id];
if (!tool) return;
view = 'detail';
els.searchWrap.style.display = 'none';
els.cats.style.display = 'none';
els.body.innerHTML = '';
const wrap = el('div', { class: 'xcc-detail' });
wrap.appendChild(el('button', { class: 'xcc-back', html: '← All tools', onclick: renderList }));
wrap.appendChild(el('div', { class: 'xcc-dtitle' }, [el('span', { text: tool.emoji }), document.createTextNode(tool.title)]));
const cat = CATEGORIES.find((c) => c.id === tool.category);
wrap.appendChild(el('div', { class: 'xcc-badges' }, [
el('span', { class: 'xcc-badge cat', text: (cat ? cat.label : tool.category) }),
el('span', { class: 'xcc-badge', style: 'background:' + DANGER[tool.danger].color + ';color:#04120c', text: DANGER[tool.danger].label })
]));
wrap.appendChild(el('div', { class: 'xcc-ddesc', text: tool.desc }));
// Where to run
const onRight = pageMatches(tool.where);
const whereBox = el('div', { class: 'xcc-where' + (onRight === false ? ' warn' : '') });
whereBox.innerHTML = '<b>Where to run:</b> ' + esc(tool.where.label) +
(tool.where.url ? ' · <a class="xcc-where-link" href="' + esc(tool.where.url) + '" target="_blank" rel="noopener">open page ↗</a>' : '') +
(onRight === false ? '<br><span style="color:#ffd400">You may not be on the right page for this tool.</span>' : '');
wrap.appendChild(whereBox);
// Danger note
if (tool.danger !== 'safe') {
wrap.appendChild(el('div', { class: (tool.danger === 'destructive' ? 'xcc-warn' : 'xcc-where') }, [
el('span', { html: (tool.danger === 'destructive' ? '⚠️ ' : '') + esc(DANGER[tool.danger].note) })
]));
}
// Options form
let form = null;
if (tool.defaults && Object.keys(tool.defaults).length) {
const details = el('details', { class: 'xcc-opts' });
details.appendChild(el('summary', { text: '⚙️ Options' }));
form = buildForm(tool.defaults);
const bar = el('div', { class: 'xcc-optbar' }, [
el('button', { text: 'Reset', onclick: () => { form.reset(); } }),
el('button', { text: form.jsonMode() ? 'Form editor' : 'Edit as JSON', onclick: (e) => { const j = form.toggleJson(); e.target.textContent = j ? 'Form editor' : 'Edit as JSON'; } })
]);
details.append(bar, form.element);
wrap.appendChild(details);
}
const runBtn = el('button', { class: 'xcc-run ' + tool.danger, text: '▶ Run ' + tool.title });
let armed = false;
runBtn.addEventListener('click', () => {
if (tool.danger === 'destructive' && !armed) {
armed = true;
runBtn.textContent = '⚠️ Click again to confirm. This cannot be undone';
setTimeout(() => { if (armed) { armed = false; runBtn.textContent = '▶ Run ' + tool.title; } }, 4000);
return;
}
let cfg = null;
if (form) {
try { cfg = form.getValue(); }
catch (e) { toast('Options are not valid JSON: ' + e.message, 'bad'); return; }
}
runTool(tool.id, cfg);
});
wrap.appendChild(runBtn);
if (tool.stopGlobal) {
wrap.appendChild(el('div', { class: 'xcc-footer', style: 'text-align:left;padding:8px 0 2px', html: 'This tool runs until done or stopped. Use <b>Stop</b> in the panel dock, or run <code>window.' + esc(tool.stopGlobal) + '()</code> in the console.' }));
}
els.body.appendChild(wrap);
renderDock();
}
function pageMatches(where) {
if (!where || !where.match) return null;
try {
const path = location.pathname + location.search;
return where.match.some((rx) => new RegExp(rx, 'i').test(path));
} catch (e) { return null; }
}
// ---- options form builder --------------------------------------------------
function buildForm(defaults) {
const root = el('div');
let jsonMode = false;
const jsonArea = el('textarea', { class: 'xcc-json', spellcheck: 'false', style: 'display:none' });
const fieldsWrap = el('div');
root.append(fieldsWrap, jsonArea);
const getters = [];
function renderFields(obj, container, prefix) {
for (const key of Object.keys(obj)) {
const val = obj[key];
const path = prefix ? prefix + '.' + key : key;
if (val === null || typeof val === 'function') continue;
if (typeof val === 'boolean') {
const input = el('input', { type: 'checkbox' });
input.checked = val;
container.appendChild(el('label', { class: 'xcc-check' }, [input, document.createTextNode(key)]));
getters.push({ path, get: () => input.checked });
} else if (typeof val === 'number') {
const input = el('input', { type: 'number', value: String(val) });
container.appendChild(el('div', { class: 'xcc-field' }, [el('label', { text: key }), input]));
getters.push({ path, get: () => { const n = Number(input.value); return Number.isFinite(n) ? n : val; } });
} else if (typeof val === 'string') {
const input = el('input', { type: 'text', value: val });
container.appendChild(el('div', { class: 'xcc-field' }, [el('label', { text: key }), input]));
getters.push({ path, get: () => input.value });
} else if (Array.isArray(val)) {
const primitive = val.every((v) => typeof v === 'string' || typeof v === 'number');
if (primitive) {
const input = el('input', { type: 'text', value: val.join(', ') });
container.appendChild(el('div', { class: 'xcc-field' }, [el('label', { text: key + ' (comma-separated)' }), input]));
getters.push({ path, get: () => input.value.split(',').map((s) => s.trim()).filter(Boolean) });
} else {
addJsonField(container, key, val, path);
}
} else if (typeof val === 'object') {
// one level of nesting as a labelled sub-section
if (!prefix) {
const sub = el('div', { class: 'xcc-sub-obj' }, [el('div', { class: 'xcc-sub-title', text: key })]);
container.appendChild(sub);
renderFields(val, sub, path);
} else {
addJsonField(container, key, val, path);
}
}
}
}
function addJsonField(container, key, val, path) {
const area = el('textarea', { class: 'xcc-json', spellcheck: 'false', style: 'min-height:60px' });
area.value = JSON.stringify(val, null, 2);
container.appendChild(el('div', { class: 'xcc-field' }, [el('label', { text: key + ' (JSON)' }), area]));
getters.push({ path, get: () => JSON.parse(area.value) });
}
function setDeep(obj, path, value) {
const parts = path.split('.');
let o = obj;
for (let i = 0; i < parts.length - 1; i++) { o[parts[i]] = o[parts[i]] || {}; o = o[parts[i]]; }
o[parts[parts.length - 1]] = value;
}
renderFields(defaults, fieldsWrap, '');
return {
element: root,
jsonMode: () => jsonMode,
toggleJson() {
jsonMode = !jsonMode;
if (jsonMode) { jsonArea.value = JSON.stringify(this.getValueFromFields(), null, 2); jsonArea.style.display = ''; fieldsWrap.style.display = 'none'; }
else { fieldsWrap.style.display = ''; jsonArea.style.display = 'none'; }
return jsonMode;
},
getValueFromFields() {
const out = clone(defaults);
for (const g of getters) { try { setDeep(out, g.path, g.get()); } catch (e) { /* keep default */ } }
return out;
},
getValue() {
if (jsonMode) { jsonArea.classList.remove('bad'); try { return JSON.parse(jsonArea.value); } catch (e) { jsonArea.classList.add('bad'); throw e; } }
return this.getValueFromFields();
},
reset() {
jsonMode = false; jsonArea.style.display = 'none'; fieldsWrap.style.display = '';
fieldsWrap.innerHTML = ''; getters.length = 0; renderFields(defaults, fieldsWrap, '');
}
};
}
// ---- running a tool --------------------------------------------------------
function runTool(id, cfg) {
const tool = byId[id];
if (!tool || typeof TOOLS[id] !== 'function') { toast('Tool not found: ' + id, 'bad'); return; }
pushRecent(id);
if (cfg) window.__XA_LAUNCH_CFG = cfg; else delete window.__XA_LAUNCH_CFG;
let started = true;
try {
TOOLS[id]();
} catch (e) {
started = false;
console.error(TAG, 'failed to start', id, e);
toast('Failed to start ' + tool.title + ': ' + e.message, 'bad');
}
// The tool has read CONFIG synchronously by now; clear the shared override.
delete window.__XA_LAUNCH_CFG;
if (!started) return;
launched.unshift({ id, title: tool.title, at: Date.now(), stopGlobal: tool.stopGlobal });
toast('▶ Launched “' + tool.title + '”. Open the Console (F12) to watch progress', 'good');
panel.classList.remove('xcc-min');
renderDock();
}
function renderDock() {
if (!launched.length) { els.dock.style.display = 'none'; els.dock.innerHTML = ''; return; }
els.dock.style.display = '';
els.dock.innerHTML = '';
const anyStoppable = launched.some((l) => l.stopGlobal);
const head = el('div', { class: 'xcc-dock-head' }, [
el('span', { text: 'Launched this session' }),
anyStoppable ? el('button', { text: 'Stop all', onclick: stopAll }) : el('button', { text: 'Clear', style: 'color:#536471', onclick: () => { launched.length = 0; renderDock(); } })
]);
els.dock.appendChild(head);
for (const item of launched.slice(0, 8)) {
const controls = item.stopGlobal
? el('button', { text: 'Stop', onclick: () => stopOne(item) })
: el('button', { class: 'ghost', text: 'Console', title: 'Progress prints to the DevTools Console', onclick: () => toast('Open DevTools → Console to see this tool’s progress') });
els.dock.appendChild(el('div', { class: 'xcc-run-item' }, [
el('span', { text: byId[item.id] ? byId[item.id].emoji : '▶' }),
el('div', { class: 'xcc-ri-meta' }, [
el('div', { class: 'xcc-ri-title', text: item.title }),
el('div', { class: 'xcc-ri-time', text: 'started ' + fmtTime(item.at) })
]),
controls
]));
}
}
function stopOne(item) {
if (item.stopGlobal && typeof window[item.stopGlobal] === 'function') {
try { window[item.stopGlobal](); toast('⏹ Stopping ' + item.title + '…'); }
catch (e) { toast('Could not stop ' + item.title + ': ' + e.message, 'bad'); }
}
}
function stopAll() {
const globals = [...new Set(launched.map((l) => l.stopGlobal).filter(Boolean))];
let n = 0;
for (const g of globals) { if (typeof window[g] === 'function') { try { window[g](); n++; } catch (e) { /* ignore */ } } }
toast(n ? '⏹ Sent stop to ' + n + ' running tool' + (n > 1 ? 's' : '') : 'Nothing running to stop');
}
// ---- keyboard --------------------------------------------------------------
function onKey(e) {
// Global reopen shortcut.
if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
if (e.shiftKey) return; // leave X's own shortcuts alone
e.preventDefault(); e.stopPropagation();
if (panel && panel.style.display !== 'none') els.search.focus();
else open();
return;
}
if (!panel || panel.style.display === 'none') return;
const inField = e.target && panel.contains(e.target) && /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName) && e.target !== els.search;
if (e.key === 'Escape') {
if (view === 'detail') { e.preventDefault(); renderList(); els.search.focus(); }
else { e.preventDefault(); close(); }
return;
}
if (view !== 'list' || inField) return;
if (e.key === 'ArrowDown') { e.preventDefault(); activeIndex = Math.min(activeIndex + 1, visibleRows.length - 1); highlight(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); activeIndex = Math.max(activeIndex - 1, 0); highlight(); }
else if (e.key === 'Enter') { const r = visibleRows[activeIndex]; if (r) { e.preventDefault(); openDetail(r.tool.id); } }
}
// ---- drag ------------------------------------------------------------------
function makeDraggable(handle) {
let sx, sy, st, sl, dragging = false;
handle.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
dragging = true;
const r = panel.getBoundingClientRect();
sx = e.clientX; sy = e.clientY; st = r.top; sl = r.left;
panel.style.right = 'auto'; panel.style.left = sl + 'px';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const top = Math.max(0, Math.min(window.innerHeight - 60, st + e.clientY - sy));
const left = Math.max(0, Math.min(window.innerWidth - 60, sl + e.clientX - sx));
panel.style.top = top + 'px'; panel.style.left = left + 'px';
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
const r = panel.getBoundingClientRect();
state.pos = { top: Math.round(r.top), left: Math.round(r.left) };
saveState();
});
}
// ---- lifecycle -------------------------------------------------------------
function open() {
if (!panel) return;
panel.style.display = 'flex';
if (fab) fab.style.display = 'none';
view = 'list';
renderList();
els.search.focus();
}
function close() {
if (!panel) return;
panel.style.display = 'none';
if (fab) fab.style.display = 'flex';
}
function destroy() {
document.removeEventListener('keydown', onKey, true);
if (panel) panel.remove();
if (fab) fab.remove();
const s = document.getElementById(PANEL_ID + '-style');
if (s) s.remove();
delete window.XActionsCommandCenter;
}
build();
console.log('%c⚡ XActions Command Center v' + VERSION, 'color:#1d9bf0;font-weight:bold;font-size:14px');
console.log('%c' + CATALOG.length + ' tools loaded. Search, pick one, and press Run. Reopen anytime with the ⚡ button or Cmd/Ctrl+K.', 'color:#8899a6');
window.XActionsCommandCenter = {
open, close, destroy,
tools: () => CATALOG.map((t) => ({ id: t.id, title: t.title, category: t.category, danger: t.danger })),
run: (id, cfg) => runTool(id, cfg || null),
version: VERSION
};
})();
⚡ More XActions Scripts
Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.
Browse All Scripts