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;">×</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})();