Roblox FriendList Follow Scraper

looks for people with 1k+ followers who sent you a friend request

  1. // ==UserScript==
  2. // @name Roblox FriendList Follow Scraper
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description looks for people with 1k+ followers who sent you a friend request
  6. // @author p_dh
  7. // @match https://www.roblox.com/users/friends
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=roblox.com
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_download
  11. // @connect roblox.com
  12. // @run-at document-end
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. const FOLLOWERS_API = 'https://friends.roblox.com/v1/users/%userId%/followers/count';
  20. const USERNAME_API = 'https://users.roblox.com/v1/users/%userId%';
  21. const PROCESSING_DELAY = 5;
  22. const BATCH_SIZE = 5;
  23. let isProcessing = false;
  24. let currentPage = 1;
  25. const validUsers = [];
  26. let startTime = null;
  27.  
  28. async function fetchUsername(userId) {
  29. return new Promise((resolve) => {
  30. GM_xmlhttpRequest({
  31. method: 'GET',
  32. url: USERNAME_API.replace('%userId%', userId),
  33. headers: { 'Accept': 'application/json' },
  34. onload: (response) => {
  35. try {
  36. const data = JSON.parse(response.responseText);
  37. resolve(data.name || `User${userId}`);
  38. } catch {
  39. resolve(`User${userId}`);
  40. }
  41. },
  42. onerror: () => resolve(`User${userId}`)
  43. });
  44. });
  45. }
  46.  
  47. async function fetchFollowerCount(userId) {
  48. return new Promise((resolve) => {
  49. GM_xmlhttpRequest({
  50. method: 'GET',
  51. url: FOLLOWERS_API.replace('%userId%', userId),
  52. headers: { 'Accept': 'application/json' },
  53. onload: (response) => {
  54. try {
  55. const data = JSON.parse(response.responseText);
  56. resolve(data.count || 0);
  57. } catch {
  58. resolve(0);
  59. }
  60. },
  61. onerror: () => resolve(0)
  62. });
  63. });
  64. }
  65.  
  66. function createStatusOverlay() {
  67. const overlay = document.createElement('div');
  68. overlay.style = `
  69. position: fixed;
  70. bottom: 20px;
  71. right: 20px;
  72. background: #1a1a1a;
  73. color: #fff;
  74. padding: 15px;
  75. border-radius: 8px;
  76. z-index: 99999;
  77. box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  78. font-family: Arial, sans-serif;
  79. display: none;
  80. `;
  81. overlay.innerHTML = `
  82. <h3 style="margin:0 0 10px 0; font-size:16px;">🔍 Checking Followers...</h3>
  83. <div class="progress-container" style="background:#333; height:4px; border-radius:2px;">
  84. <div class="progress-bar" style="height:100%; background:#00a8fc; width:0; transition:width 0.3s"></div>
  85. </div>
  86. <div class="current-user" style="margin-top:10px; font-size:14px;">Checking: <span id="currentUser">-</span></div>
  87. <div class="results" style="margin-top:10px;"></div>
  88. <div class="timer" style="margin-top:10px; font-size:12px; color:#888;">Time Elapsed: 0s</div>
  89. <button id="exportButton" style="margin-top:10px; padding:5px 10px; background:#00a8fc; color:#fff; border:none; border-radius:4px; cursor:pointer; display:none;">Export Results as TXT</button>
  90. <div style="margin-top:10px; font-size:12px; color:#888; text-align:center;">made for my cutie pie reme by p_dh 🤤❤</div>
  91. `;
  92. document.body.appendChild(overlay);
  93. return overlay;
  94. }
  95.  
  96. function exportResults() {
  97. const sortedUsers = validUsers.sort((a, b) => b.followers - a.followers); // Sort high to low
  98. const content = sortedUsers.map(user => `${user.username} (${user.followers})`).join('\n');
  99. const blob = new Blob([content], { type: 'text/plain' });
  100. const url = URL.createObjectURL(blob);
  101. GM_download({
  102. url: url,
  103. name: 'valid_users.txt',
  104. saveAs: true,
  105. onload: () => URL.revokeObjectURL(url)
  106. });
  107. }
  108.  
  109. function formatTimeElapsed(seconds) {
  110. const minutes = Math.floor(seconds / 60);
  111. const secs = seconds % 60;
  112. return `${minutes}m ${secs}s`;
  113. }
  114.  
  115. async function processRequests() {
  116. if (isProcessing) return;
  117. isProcessing = true;
  118.  
  119. const overlay = createStatusOverlay();
  120. overlay.style.display = 'block';
  121. const progressBar = overlay.querySelector('.progress-bar');
  122. const resultsContainer = overlay.querySelector('.results');
  123. const timerContainer = overlay.querySelector('.timer');
  124. const exportButton = overlay.querySelector('#exportButton');
  125. const currentUserElement = overlay.querySelector('#currentUser');
  126.  
  127. startTime = Date.now();
  128. const timerInterval = setInterval(() => {
  129. const elapsed = Math.floor((Date.now() - startTime) / 1000);
  130. timerContainer.textContent = `Time Elapsed: ${formatTimeElapsed(elapsed)}`;
  131. }, 1000);
  132.  
  133. try {
  134. while (true) {
  135. const requests = Array.from(document.querySelectorAll('.list-item.avatar-card:not(.disabled)'));
  136. const total = requests.length;
  137. let processed = 0;
  138.  
  139. for (let i = 0; i < total; i += BATCH_SIZE) {
  140. const batch = requests.slice(i, i + BATCH_SIZE);
  141. const batchPromises = batch.map(async (request) => {
  142. const profileLink = request.querySelector('.avatar-card-link')?.href;
  143.  
  144. if (!profileLink || profileLink.includes('/banned-users/')) {
  145. processed++;
  146. return;
  147. }
  148.  
  149. const userIdMatch = profileLink.match(/\/users\/(\d+)\//);
  150. if (!userIdMatch) {
  151. processed++;
  152. return;
  153. }
  154.  
  155. const userId = userIdMatch[1];
  156.  
  157. currentUserElement.textContent = `User${userId}`; // Display current user being checked
  158.  
  159. const [username, followers] = await Promise.all([
  160. fetchUsername(userId),
  161. fetchFollowerCount(userId)
  162. ]);
  163.  
  164. currentUserElement.textContent = username; // Update to show the actual username
  165.  
  166. if (followers > 1000) {
  167. validUsers.push({ username, followers });
  168. resultsContainer.innerHTML += `<li>${username} (${followers})</li>`; // Append to the list in real-time
  169. }
  170.  
  171. processed++;
  172. progressBar.style.width = `${(processed / total) * 100}%`;
  173. });
  174.  
  175. await Promise.all(batchPromises);
  176. await new Promise(r => setTimeout(r, PROCESSING_DELAY));
  177. }
  178.  
  179. const nextButton = document.querySelector('.btn-generic-right-sm:not(:disabled)');
  180. if (!nextButton) break;
  181.  
  182. nextButton.click();
  183. currentPage++;
  184. await waitForPageLoad();
  185. }
  186.  
  187. progressBar.style.backgroundColor = '#00c851';
  188. exportButton.style.display = 'block';
  189. exportButton.addEventListener('click', exportResults);
  190.  
  191. } catch (error) {
  192. resultsContainer.innerHTML = `<span style="color:#ff4444">Error: ${error.message}</span>`;
  193. } finally {
  194. clearInterval(timerInterval);
  195. isProcessing = false;
  196. }
  197. }
  198.  
  199. function waitForPageLoad() {
  200. return new Promise((resolve) => {
  201. const observer = new MutationObserver((mutations, obs) => {
  202. const requests = document.querySelectorAll('.list-item.avatar-card');
  203. if (requests.length > 0) {
  204. obs.disconnect();
  205. resolve();
  206. }
  207. });
  208. observer.observe(document.body, { childList: true, subtree: true });
  209. });
  210. }
  211.  
  212. document.addEventListener('keydown', (e) => {
  213. if (e.key === 'F' && e.ctrlKey && e.shiftKey) {
  214. processRequests();
  215. }
  216. });
  217. })();