Twitter Inactive Following Scanner

Scan your Twitter following list to identify inactive accounts by activity period

Size

17.3 KB

Version

1.1.1

Created

Feb 3, 2026

Updated

2 months ago

1// ==UserScript==
2// @name		Twitter Inactive Following Scanner
3// @description		Scan your Twitter following list to identify inactive accounts by activity period
4// @version		1.1.1
5// @match		https://*.x.com/*
6// @icon		https://abs.twimg.com/favicons/twitter-pip.3.ico
7// @grant		GM.xmlhttpRequest
8// @grant		GM.getValue
9// @grant		GM.setValue
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    // Utility function to debounce
15    function debounce(func, wait) {
16        let timeout;
17        return function executedFunction(...args) {
18            const later = () => {
19                clearTimeout(timeout);
20                func(...args);
21            };
22            clearTimeout(timeout);
23            timeout = setTimeout(later, wait);
24        };
25    }
26
27    // Function to get username from URL
28    function getUsernameFromUrl() {
29        const match = window.location.pathname.match(/^\/([^\/]+)\/following/);
30        return match ? match[1] : null;
31    }
32
33    // Function to check if we're on the following page
34    function isFollowingPage() {
35        return window.location.pathname.includes('/following');
36    }
37
38    // Function to fetch user's last tweet date
39    async function fetchLastTweetDate(username) {
40        return new Promise((resolve, reject) => {
41            GM.xmlhttpRequest({
42                method: 'GET',
43                url: `https://x.com/${username}`,
44                onload: function(response) {
45                    try {
46                        const parser = new DOMParser();
47                        const doc = parser.parseFromString(response.responseText, 'text/html');
48                        
49                        // Try to find tweet timestamps in the HTML
50                        const timeElements = doc.querySelectorAll('time[datetime]');
51                        
52                        if (timeElements.length > 0) {
53                            // Get the most recent tweet date
54                            const dates = Array.from(timeElements).map(el => new Date(el.getAttribute('datetime')));
55                            const mostRecent = new Date(Math.max(...dates));
56                            resolve(mostRecent);
57                        } else {
58                            // No tweets found, account might be inactive or protected
59                            resolve(null);
60                        }
61                    } catch (error) {
62                        console.error(`Error parsing profile for ${username}:`, error);
63                        resolve(null);
64                    }
65                },
66                onerror: function(error) {
67                    console.error(`Error fetching profile for ${username}:`, error);
68                    resolve(null);
69                }
70            });
71        });
72    }
73
74    // Function to collect all usernames from the following list
75    async function collectAllUsernames() {
76        const usernames = new Set();
77        let previousCount = 0;
78        let stableCount = 0;
79        const maxStableIterations = 5;
80
81        console.log('Starting to collect usernames...');
82
83        return new Promise((resolve) => {
84            const checkInterval = setInterval(() => {
85                // Find all user cells
86                const userCells = document.querySelectorAll('[data-testid="UserCell"]');
87                
88                userCells.forEach(cell => {
89                    const link = cell.querySelector('a[href^="/"]');
90                    if (link) {
91                        const href = link.getAttribute('href');
92                        const username = href.replace('/', '').split('/')[0];
93                        if (username && !username.includes('?') && username.length > 0) {
94                            usernames.add(username);
95                        }
96                    }
97                });
98
99                console.log(`Collected ${usernames.size} usernames so far...`);
100
101                // Check if we've stopped finding new users
102                if (usernames.size === previousCount) {
103                    stableCount++;
104                    if (stableCount >= maxStableIterations) {
105                        clearInterval(checkInterval);
106                        console.log(`Finished collecting ${usernames.size} usernames`);
107                        resolve(Array.from(usernames));
108                    }
109                } else {
110                    stableCount = 0;
111                    previousCount = usernames.size;
112                }
113
114                // Scroll to load more
115                window.scrollTo(0, document.body.scrollHeight);
116            }, 2000);
117        });
118    }
119
120    // Function to categorize users by inactivity
121    function categorizeByInactivity(usersData) {
122        const now = new Date();
123        const threeMonthsAgo = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000));
124        const sixMonthsAgo = new Date(now.getTime() - (180 * 24 * 60 * 60 * 1000));
125        const oneYearAgo = new Date(now.getTime() - (365 * 24 * 60 * 60 * 1000));
126
127        const categories = {
128            active: [],
129            inactive3Months: [],
130            inactive6Months: [],
131            inactive1Year: [],
132            inactiveMoreThan1Year: [],
133            noData: []
134        };
135
136        usersData.forEach(user => {
137            if (!user.lastTweetDate) {
138                categories.noData.push(user);
139            } else {
140                const lastTweet = new Date(user.lastTweetDate);
141                
142                if (lastTweet >= threeMonthsAgo) {
143                    categories.active.push(user);
144                } else if (lastTweet >= sixMonthsAgo) {
145                    categories.inactive3Months.push(user);
146                } else if (lastTweet >= oneYearAgo) {
147                    categories.inactive6Months.push(user);
148                } else {
149                    categories.inactiveMoreThan1Year.push(user);
150                }
151            }
152        });
153
154        return categories;
155    }
156
157    // Function to create and display results UI
158    function displayResults(categories) {
159        // Remove existing results panel if any
160        const existingPanel = document.getElementById('inactive-scanner-panel');
161        if (existingPanel) {
162            existingPanel.remove();
163        }
164
165        // Create results panel
166        const panel = document.createElement('div');
167        panel.id = 'inactive-scanner-panel';
168        panel.style.cssText = `
169            position: fixed;
170            top: 50%;
171            left: 50%;
172            transform: translate(-50%, -50%);
173            background: #15202b;
174            color: #ffffff;
175            border: 1px solid #38444d;
176            border-radius: 16px;
177            padding: 20px;
178            max-width: 600px;
179            max-height: 80vh;
180            overflow-y: auto;
181            z-index: 10000;
182            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
183        `;
184
185        const totalInactive = categories.inactive3Months.length + 
186                             categories.inactive6Months.length + 
187                             categories.inactive1Year.length + 
188                             categories.inactiveMoreThan1Year.length;
189
190        panel.innerHTML = `
191            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
192                <h2 style="margin: 0; font-size: 20px; font-weight: bold;">Inactive Following Scanner Results</h2>
193                <button id="close-scanner-panel" style="background: none; border: none; color: #8899a6; cursor: pointer; font-size: 24px; padding: 0; width: 30px; height: 30px;">&times;</button>
194            </div>
195            
196            <div style="margin-bottom: 20px; padding: 15px; background: #192734; border-radius: 8px;">
197                <div style="font-size: 16px; margin-bottom: 10px;">
198                    <strong>Total Inactive Accounts:</strong> ${totalInactive}
199                </div>
200            </div>
201
202            <div style="margin-bottom: 15px;">
203                <div style="background: #192734; padding: 12px; border-radius: 8px; margin-bottom: 10px;">
204                    <div style="font-weight: bold; margin-bottom: 8px; color: #1da1f2;">
205                        Inactive 3-6 Months (${categories.inactive3Months.length})
206                    </div>
207                    <div style="max-height: 150px; overflow-y: auto;">
208                        ${categories.inactive3Months.length > 0 ? 
209        categories.inactive3Months.map(u => `
210                                <div style="padding: 5px 0; border-bottom: 1px solid #38444d;">
211                                    <a href="https://x.com/${u.username}" target="_blank" style="color: #1da1f2; text-decoration: none;">@${u.username}</a>
212                                    <span style="color: #8899a6; font-size: 12px; margin-left: 10px;">
213                                        Last: ${u.lastTweetDate ? new Date(u.lastTweetDate).toLocaleDateString() : 'Unknown'}
214                                    </span>
215                                </div>
216                            `).join('') : 
217        '<div style="color: #8899a6; font-size: 14px;">No accounts in this category</div>'
218    }
219                    </div>
220                </div>
221
222                <div style="background: #192734; padding: 12px; border-radius: 8px; margin-bottom: 10px;">
223                    <div style="font-weight: bold; margin-bottom: 8px; color: #ffad1f;">
224                        Inactive 6-12 Months (${categories.inactive6Months.length})
225                    </div>
226                    <div style="max-height: 150px; overflow-y: auto;">
227                        ${categories.inactive6Months.length > 0 ? 
228        categories.inactive6Months.map(u => `
229                                <div style="padding: 5px 0; border-bottom: 1px solid #38444d;">
230                                    <a href="https://x.com/${u.username}" target="_blank" style="color: #1da1f2; text-decoration: none;">@${u.username}</a>
231                                    <span style="color: #8899a6; font-size: 12px; margin-left: 10px;">
232                                        Last: ${u.lastTweetDate ? new Date(u.lastTweetDate).toLocaleDateString() : 'Unknown'}
233                                    </span>
234                                </div>
235                            `).join('') : 
236        '<div style="color: #8899a6; font-size: 14px;">No accounts in this category</div>'
237    }
238                    </div>
239                </div>
240
241                <div style="background: #192734; padding: 12px; border-radius: 8px; margin-bottom: 10px;">
242                    <div style="font-weight: bold; margin-bottom: 8px; color: #f91880;">
243                        Inactive 1+ Years (${categories.inactiveMoreThan1Year.length})
244                    </div>
245                    <div style="max-height: 150px; overflow-y: auto;">
246                        ${categories.inactiveMoreThan1Year.length > 0 ? 
247        categories.inactiveMoreThan1Year.map(u => `
248                                <div style="padding: 5px 0; border-bottom: 1px solid #38444d;">
249                                    <a href="https://x.com/${u.username}" target="_blank" style="color: #1da1f2; text-decoration: none;">@${u.username}</a>
250                                    <span style="color: #8899a6; font-size: 12px; margin-left: 10px;">
251                                        Last: ${u.lastTweetDate ? new Date(u.lastTweetDate).toLocaleDateString() : 'Unknown'}
252                                    </span>
253                                </div>
254                            `).join('') : 
255        '<div style="color: #8899a6; font-size: 14px;">No accounts in this category</div>'
256    }
257                    </div>
258                </div>
259
260                ${categories.noData.length > 0 ? `
261                    <div style="background: #192734; padding: 12px; border-radius: 8px;">
262                        <div style="font-weight: bold; margin-bottom: 8px; color: #8899a6;">
263                            No Data Available (${categories.noData.length})
264                        </div>
265                        <div style="max-height: 100px; overflow-y: auto; font-size: 12px; color: #8899a6;">
266                            Protected or suspended accounts
267                        </div>
268                    </div>
269                ` : ''}
270            </div>
271        `;
272
273        document.body.appendChild(panel);
274
275        // Add close button functionality
276        document.getElementById('close-scanner-panel').addEventListener('click', () => {
277            panel.remove();
278        });
279    }
280
281    // Function to create scan button
282    function createScanButton() {
283        // Check if button already exists
284        if (document.getElementById('inactive-scanner-btn')) {
285            return;
286        }
287
288        // Find the primary column
289        const primaryColumn = document.querySelector('[data-testid="primaryColumn"]');
290        if (!primaryColumn) {
291            console.log('Primary column not found');
292            return;
293        }
294
295        // Find the timeline container
296        const timeline = primaryColumn.querySelector('[aria-label*="Timeline"]');
297        if (!timeline) {
298            console.log('Timeline not found');
299            return;
300        }
301
302        // Create button container
303        const buttonContainer = document.createElement('div');
304        buttonContainer.id = 'inactive-scanner-container';
305        buttonContainer.style.cssText = `
306            padding: 12px 16px;
307            border-bottom: 1px solid #38444d;
308            background: #15202b;
309        `;
310
311        const scanButton = document.createElement('button');
312        scanButton.id = 'inactive-scanner-btn';
313        scanButton.textContent = 'Scan for Inactive Accounts';
314        scanButton.style.cssText = `
315            background: #1da1f2;
316            color: white;
317            border: none;
318            border-radius: 9999px;
319            padding: 12px 24px;
320            font-size: 15px;
321            font-weight: bold;
322            cursor: pointer;
323            width: 100%;
324            transition: background 0.2s;
325        `;
326
327        scanButton.addEventListener('mouseenter', () => {
328            scanButton.style.background = '#1a8cd8';
329        });
330
331        scanButton.addEventListener('mouseleave', () => {
332            scanButton.style.background = '#1da1f2';
333        });
334
335        scanButton.addEventListener('click', async () => {
336            scanButton.disabled = true;
337            scanButton.textContent = 'Scanning...';
338            scanButton.style.background = '#8899a6';
339
340            try {
341                // Collect all usernames
342                const usernames = await collectAllUsernames();
343                console.log(`Collected ${usernames.length} usernames, now fetching activity data...`);
344
345                // Update button with progress
346                scanButton.textContent = `Checking activity (0/${usernames.length})...`;
347
348                // Fetch last tweet dates for all users
349                const usersData = [];
350                for (let i = 0; i < usernames.length; i++) {
351                    const username = usernames[i];
352                    scanButton.textContent = `Checking activity (${i + 1}/${usernames.length})...`;
353                    
354                    const lastTweetDate = await fetchLastTweetDate(username);
355                    usersData.push({
356                        username: username,
357                        lastTweetDate: lastTweetDate
358                    });
359
360                    // Add a small delay to avoid rate limiting
361                    await new Promise(resolve => setTimeout(resolve, 500));
362                }
363
364                // Categorize users
365                const categories = categorizeByInactivity(usersData);
366
367                // Display results
368                displayResults(categories);
369
370                // Reset button
371                scanButton.disabled = false;
372                scanButton.textContent = 'Scan for Inactive Accounts';
373                scanButton.style.background = '#1da1f2';
374
375            } catch (error) {
376                console.error('Error during scan:', error);
377                alert('An error occurred during the scan. Please try again.');
378                scanButton.disabled = false;
379                scanButton.textContent = 'Scan for Inactive Accounts';
380                scanButton.style.background = '#1da1f2';
381            }
382        });
383
384        buttonContainer.appendChild(scanButton);
385
386        // Insert button at the beginning of the timeline
387        timeline.insertBefore(buttonContainer, timeline.firstChild);
388        console.log('Scan button created successfully');
389    }
390
391    // Initialize the extension
392    function init() {
393        console.log('Init called, pathname:', window.location.pathname);
394        console.log('Is following page:', isFollowingPage());
395        
396        if (isFollowingPage()) {
397            console.log('On following page, will create button in 2 seconds');
398            // Wait for the page to load
399            setTimeout(() => {
400                console.log('Attempting to create scan button...');
401                createScanButton();
402            }, 2000);
403
404            // Watch for navigation changes
405            const observer = new MutationObserver(debounce(() => {
406                if (isFollowingPage()) {
407                    createScanButton();
408                }
409            }, 1000));
410
411            observer.observe(document.body, {
412                childList: true,
413                subtree: true
414            });
415        }
416    }
417
418    // Start the extension when the page is ready
419    if (document.readyState === 'loading') {
420        document.addEventListener('DOMContentLoaded', init);
421    } else {
422        init();
423    }
424
425    console.log('Twitter Inactive Following Scanner loaded');
426})();
Twitter Inactive Following Scanner | Robomonkey