Roblox Friends/Following/Followers List Bypass

Displays Roblox friends, followers, and following in simple UI.

// ==UserScript==
// @name         Roblox Friends/Following/Followers List Bypass
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Displays Roblox friends, followers, and following in simple UI.
// @author       @xss https://www.roblox.com/users/4245248422/profile
// @match        https://www.roblox.com/users/*/profile*
// @icon         https://www.google.com/s2/favicons?domain=roblox.com
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const modalStyle = `
        .friends-modal {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 400px;
            background: #2d2d2d;
            border-radius: 8px;
            z-index: 9999;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            color: white;
            font-family: 'Gotham SSm', Arial, sans-serif;
        }
        .modal-header {
            padding: 16px;
            border-bottom: 1px solid #404040;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .modal-title {
            font-weight: 600;
            font-size: 18px;
        }
        .modal-close {
            cursor: pointer;
            padding: 4px;
            font-size: 20px;
        }
        .friends-list {
            max-height: 60vh;
            overflow-y: auto;
            padding: 8px 0;
        }
        .friend-item {
            padding: 12px 16px;
            display: flex;
            align-items: center;
            cursor: pointer;
            transition: background 0.2s;
        }
        .friend-item:hover {
            background: #3a3a3a;
        }
        .friend-name {
            margin-left: 12px;
            font-weight: 500;
        }
        .loading {
            padding: 20px;
            text-align: center;
            color: #888;
        }
        .load-more {
            display: block;
            text-align: center;
            padding: 12px;
            background: #3a3a3a;
            cursor: pointer;
            margin: 8px 16px;
            border-radius: 4px;
            transition: background 0.2s;
        }
        .load-more:hover {
            background: #4a4a4a;
        }
        .overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            z-index: 9998;
        }
    `;

    function createModal(title) {
        const style = document.createElement('style');
        style.textContent = modalStyle;
        document.head.appendChild(style);

        const overlay = document.createElement('div');
        overlay.className = 'overlay';

        const modal = document.createElement('div');
        modal.className = 'friends-modal';

        modal.innerHTML = `
            <div class="modal-header">
                <div class="modal-title">${title}</div>
                <div class="modal-close">×</div>
            </div>
            <div class="friends-list">
                <div class="loading">Loading...</div>
            </div>
        `;

        document.body.appendChild(overlay);
        document.body.appendChild(modal);

        modal.querySelector('.modal-close').addEventListener('click', closeModal);
        overlay.addEventListener('click', closeModal);

        return modal;
    }

    function closeModal() {
        document.querySelector('.friends-modal')?.remove();
        document.querySelector('.overlay')?.remove();
    }

    // Get user details from list of IDs
    async function getUserDetails(userIds) {
        if (userIds.length === 0) return [];

        // Use Roblox's batch API to get user details
        const response = await fetch('https://users.roblox.com/v1/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                userIds: userIds,
                excludeBannedUsers: false
            })
        });

        const data = await response.json();
        return data.data || [];
    }

    // Display friends list (already has names)
    function showFriendsList(friends) {
        const list = document.querySelector('.friends-list');
        list.innerHTML = '';

        if (friends.length === 0) {
            list.innerHTML = '<div class="loading">No friends found</div>';
            return;
        }

        friends.forEach(friend => {
            const item = document.createElement('div');
            item.className = 'friend-item';

            item.innerHTML = `
                <span class="friend-name">${friend.displayName || friend.name}</span>
            `;

            item.addEventListener('click', (e) => {
                window.open(`https://www.roblox.com/users/${friend.id}/profile`, '_blank');
            });

            list.appendChild(item);
        });
    }

    // Fetch all pages of followers/following
    async function fetchAllRelations(userId, type) {
        let allRelations = [];
        let cursor = null;
        let hasMore = true;
        let page = 1;
        const maxPages = 5; // Limit to 50 results (5 pages × 10 items)

        const list = document.querySelector('.friends-list');

        while (hasMore && page <= maxPages) {
            try {
                const url = `https://friends.roblox.com/v1/users/${userId}/${type}${cursor ? `?cursor=${cursor}` : ''}`;
                const response = await fetch(url);
                const data = await response.json();

                if (data.data && data.data.length > 0) {
                    allRelations = [...allRelations, ...data.data];

                    // Update loading message
                    list.innerHTML = `<div class="loading">Loading... (Found ${allRelations.length} so far)</div>`;

                    // Check if there's more to load
                    if (data.nextPageCursor) {
                        cursor = data.nextPageCursor;
                        page++;
                    } else {
                        hasMore = false;
                    }
                } else {
                    hasMore = false;
                }
            } catch (error) {
                console.error(`Error fetching ${type}:`, error);
                hasMore = false;
            }
        }

        return allRelations;
    }

    // Display followers/following with pagination
    async function showRelationsList(userId, type, title) {
        const list = document.querySelector('.friends-list');
        list.innerHTML = `<div class="loading">Loading ${title.toLowerCase()}...</div>`;

        try {
            // Fetch all pages
            const relations = await fetchAllRelations(userId, type);

            if (relations.length === 0) {
                list.innerHTML = `<div class="loading">No ${title.toLowerCase()} found</div>`;
                return;
            }

            list.innerHTML = `<div class="loading">Found ${relations.length} ${title.toLowerCase()}. Loading details...</div>`;

            // Process in batches of 100 to avoid API limits
            const batchSize = 100;
            for (let i = 0; i < relations.length; i += batchSize) {
                const batch = relations.slice(i, i + batchSize);
                const userIds = batch.map(item => item.id);

                const userDetails = await getUserDetails(userIds);

                // If this is the first batch, clear the list
                if (i === 0) {
                    list.innerHTML = '';
                }

                // Append this batch of users
                userDetails.forEach(user => {
                    const item = document.createElement('div');
                    item.className = 'friend-item';

                    item.innerHTML = `
                        <span class="friend-name">${user.displayName || user.name}</span>
                    `;

                    item.addEventListener('click', (e) => {
                        window.open(`https://www.roblox.com/users/${user.id}/profile`, '_blank');
                    });

                    list.appendChild(item);
                });

                // Show progress if there are more batches
                if (i + batchSize < relations.length) {
                    const progressElement = document.createElement('div');
                    progressElement.className = 'loading';
                    progressElement.textContent = `Loaded ${i + batch.length} of ${relations.length}...`;
                    list.appendChild(progressElement);

                    // Remove the progress element after next batch is loaded
                    setTimeout(() => {
                        progressElement.remove();
                    }, 100);
                }
            }

            // Add a note if we hit the maximum
            if (relations.length === 50) {
                const note = document.createElement('div');
                note.className = 'loading';
                note.textContent = 'Showing 50 results';
                list.appendChild(note);
            }

        } catch (error) {
            list.innerHTML = `<div class="loading">Failed to load ${title.toLowerCase()} details</div>`;
            console.error(error);
        }
    }

    function createButton(text, color, hoverColor) {
        const button = document.createElement('div');
        button.textContent = text;
        button.style.cssText = `
            display: inline-flex;
            align-items: center;
            padding: 6px 12px;
            background-color: ${color};
            color: white;
            border-radius: 4px;
            font-family: 'Gotham SSm', Arial, sans-serif;
            font-size: 13px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
            margin-left: 12px;
            vertical-align: middle;
            position: relative;
            top: -2px;
        `;

        button.addEventListener('mouseover', () => {
            button.style.backgroundColor = hoverColor;
            button.style.transform = 'translateY(-1px)';
        });
        button.addEventListener('mouseout', () => {
            button.style.backgroundColor = color;
            button.style.transform = 'none';
        });

        return button;
    }

    function addButtons() {
        const userId = window.location.pathname.split('/')[2];
        const nameContainer = document.querySelector('.profile-header-title-container');

        if (!nameContainer || nameContainer.querySelector('.friends-list-button')) return;

        // Friends Button
        const friendsButton = createButton('Friends', '#ff69b4', '#e0529c');
        friendsButton.className = 'friends-list-button';
        friendsButton.addEventListener('click', async (e) => {
            e.stopPropagation();
            createModal('Friends List');
            try {
                const response = await fetch(`https://friends.roblox.com/v1/users/${userId}/friends`);
                const data = await response.json();
                showFriendsList(data.data);
            } catch (error) {
                document.querySelector('.friends-list').innerHTML =
                    '<div class="loading">Failed to load friends list</div>';
            }
        });

        // Followers Button
        const followersButton = createButton('Followers', '#5865F2', '#4752c4');
        followersButton.addEventListener('click', async (e) => {
            e.stopPropagation();
            createModal('Followers');
            showRelationsList(userId, 'followers', 'Followers');
        });

        // Following Button
        const followingButton = createButton('Following', '#57A559', '#458a47');
        followingButton.addEventListener('click', async (e) => {
            e.stopPropagation();
            createModal('Following');
            showRelationsList(userId, 'followings', 'Following');
        });

        nameContainer.appendChild(friendsButton);
        nameContainer.appendChild(followersButton);
        nameContainer.appendChild(followingButton);
    }

    // Run when the page is fully loaded
    window.addEventListener('load', addButtons);

    // Also run on navigation (for when users navigate between profiles)
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(addButtons, 500);
        }
    }).observe(document, {subtree: true, childList: true});
})();