Roblox Mass Unfriend with Recovery (Fixed v3.1)

Unfriend all Roblox friends with option to recover – Updated to use Roblox Friends API

// ==UserScript==
// @name         Roblox Mass Unfriend with Recovery (Fixed v3.1)
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Unfriend all Roblox friends with option to recover – Updated to use Roblox Friends API
// @license      MIT
// @author       TheBestChillieDog
// @match        https://www.roblox.com/users/*/friends*
// @match        https://www.roblox.com/my/account#!/friends
// @match        https://www.roblox.com/users/friends
// @match        https://www.roblox.com/home/friends*
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      roblox.com
// @connect      friends.roblox.com
// @connect      users.roblox.com
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(function() {
    'use strict';

    //
    // ─── CONFIGURATION ────────────────────────────────────────────────────────────
    //

    const DELAY_BETWEEN_REQUESTS = 1500;       // 1.5s delay between each unfriend/friend request
    const BACKUP_KEY             = 'roblox_friends_backup_v3';
    const MAX_RETRIES            = 5;          // for API calls
    const LOAD_WAIT_TIME         = 1000;       // (not actually used any more for DOM scanning)

    GM_addStyle(`
        .unfriend-controls {
            background: #2a2a2a;
            border: 1px solid #444;
            border-radius: 6px;
            padding: 20px;
            margin: 20px 0;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            color: #fff;
            position: relative;
            z-index: 10000;
        }
        .unfriend-controls h3 {
            margin-top: 0;
            color: #fff;
            border-bottom: 1px solid #444;
            padding-bottom: 10px;
        }
        .unfriend-btn, .recover-btn, .scan-btn, .debug-btn {
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.3s;
        }
        .unfriend-btn {
            background-color: #e74c3c;
            padding: 12px 20px;
            margin-right: 12px;
            margin-bottom: 10px;
            font-size: 14px;
        }
        .unfriend-btn:hover:not(:disabled) {
            background-color: #c0392b;
            transform: translateY(-2px);
        }
        .unfriend-btn:disabled {
            background-color: #666;
            cursor: not-allowed;
            transform: none;
        }
        .recover-btn {
            background-color: #2ecc71;
            padding: 12px 20px;
            margin-bottom: 10px;
            font-size: 14px;
        }
        .recover-btn:hover:not(:disabled) {
            background-color: #27ae60;
            transform: translateY(-2px);
        }
        .recover-btn:disabled {
            background-color: #666;
            cursor: not-allowed;
            transform: none;
        }
        .scan-btn {
            background-color: #f39c12;
            padding: 10px 16px;
            font-size: 12px;
            margin-right: 10px;
        }
        .scan-btn:hover:not(:disabled) {
            background-color: #d35400;
            transform: translateY(-1px);
        }
        .scan-btn:disabled {
            background-color: #666;
            cursor: not-allowed;
            transform: none;
        }
        .debug-btn {
            background-color: #3498db;
            padding: 8px 16px;
            font-size: 12px;
            margin-left: 10px;
        }
        .debug-btn:hover:not(:disabled) {
            background-color: #2980b9;
            transform: translateY(-1px);
        }
        .debug-btn:disabled {
            background-color: #666;
            cursor: not-allowed;
            transform: none;
        }
        .progress-container {
            margin-top: 20px;
            display: none;
            background: #333;
            padding: 15px;
            border-radius: 4px;
        }
        .progress-bar {
            height: 20px;
            background: linear-gradient(90deg, #3498db, #2ecc71);
            border-radius: 4px;
            width: 0%;
            transition: width 0.5s;
            margin-bottom: 10px;
        }
        .status-text {
            margin-top: 5px;
            font-size: 14px;
            color: #bbb;
        }
        .friend-selection {
            max-height: 400px;
            overflow-y: auto;
            border: 1px solid #444;
            padding: 15px;
            margin-top: 20px;
            display: none;
            background: #333;
            border-radius: 4px;
        }
        .friend-selection h4 {
            margin-top: 0;
            color: #fff;
        }
        .friend-item {
            display: flex;
            align-items: center;
            padding: 10px 0;
            border-bottom: 1px solid #444;
        }
        .friend-item:last-child {
            border-bottom: none;
        }
        .friend-checkbox {
            margin-right: 15px;
            transform: scale(1.2);
        }
        .friend-username {
            flex-grow: 1;
            color: #fff;
        }
        .select-all-container {
            margin-bottom: 15px;
        }
        .warning-message {
            color: #e74c3c;
            font-weight: bold;
            margin: 15px 0;
        }
        .debug-info {
            background: #1a1a1a;
            border: 1px solid #333;
            padding: 10px;
            margin: 10px 0;
            border-radius: 4px;
            font-family: monospace;
            font-size: 12px;
            color: #ccc;
            max-height: 300px;
            overflow-y: auto;
            display: none;
            white-space: pre-wrap;
        }
    `);

    //
    // ─── GLOBAL UI SETUP ─────────────────────────────────────────────────────────────
    //

    const controlsDiv = document.createElement('div');
    controlsDiv.className = 'unfriend-controls';
    controlsDiv.innerHTML = `
        <h3>
            Roblox Mass Unfriend Tool 
            <button class="scan-btn" id="scanBtn">Scan Friends</button>
            <button class="debug-btn" id="debugBtn">Debug Info</button>
        </h3>
        <div class="warning-message">
            ⚠️ WARNING: This will permanently remove friends. A backup will be saved for recovery.
        </div>
        <button class="unfriend-btn" id="unfriendAllBtn">Unfriend All Friends</button>
        <button class="recover-btn" id="recoverFriendsBtn">Recover Friends</button>
        <div class="debug-info" id="debugInfo"></div>
        <div class="progress-container" id="progressContainer">
            <div class="progress-bar" id="progressBar"></div>
            <div class="status-text" id="statusText"></div>
        </div>
        <div class="friend-selection" id="friendSelection">
            <h4>Select Friends to Recover</h4>
            <div class="select-all-container">
                <input type="checkbox" id="selectAllFriends" checked>
                <label for="selectAllFriends">Select All Friends</label>
            </div>
            <div id="friendsList"></div>
            <button class="recover-btn" id="confirmRecoverBtn" style="margin-top: 15px;">Confirm Recovery</button>
        </div>
    `;

    // Try to insert control panel into a likely container
    function insertControlPanel() {
        const containers = [
            document.querySelector('main'),
            document.querySelector('.content'),
            document.querySelector('.container-main'),
            document.querySelector('#content'),
            document.querySelector('.page-content'),
            document.querySelector('body')
        ].filter(el => el !== null);

        if (containers.length > 0) {
            containers[0].insertBefore(controlsDiv, containers[0].firstChild);
            return true;
        }
        return false;
    }

    let insertAttempts = 0;
    const insertInterval = setInterval(() => {
        if (insertControlPanel() || insertAttempts++ > 15) {
            clearInterval(insertInterval);
        }
    }, 500);

    function getUIElements() {
        return {
            unfriendAllBtn: document.getElementById('unfriendAllBtn'),
            recoverFriendsBtn: document.getElementById('recoverFriendsBtn'),
            progressContainer: document.getElementById('progressContainer'),
            progressBar: document.getElementById('progressBar'),
            statusText: document.getElementById('statusText'),
            friendSelection: document.getElementById('friendSelection'),
            friendsList: document.getElementById('friendsList'),
            confirmRecoverBtn: document.getElementById('confirmRecoverBtn'),
            selectAllCheckbox: document.getElementById('selectAllFriends'),
            debugBtn: document.getElementById('debugBtn'),
            debugInfo: document.getElementById('debugInfo'),
            scanBtn: document.getElementById('scanBtn')
        };
    }

    //
    // ─── DEBUG INFO ───────────────────────────────────────────────────────────────────
    //

    function showDebugInfo() {
        const ui = getUIElements();
        const debugInfo = ui.debugInfo;
        if (!debugInfo) return;

        const isVisible = debugInfo.style.display !== 'block';
        debugInfo.style.display = isVisible ? 'block' : 'none';

        if (isVisible) {
            let debugText = 'DOM Analysis:\n\n';

            const friendSelectors = [
                'a[href*="/users/"]',
                '*[class*="friend"]',
                '*[data-testid*="friend"]',
                '.avatar',
                '*[class*="list"]'
            ];

            friendSelectors.forEach(selector => {
                try {
                    const elements = document.querySelectorAll(selector);
                    debugText += `${selector}: ${elements.length} elements\n`;
                    if (elements.length > 0 && elements.length < 5) {
                        Array.from(elements).forEach((el, i) => {
                            debugText += `  [${i+1}] ${el.tagName} class="${el.className}" href="${el.href || 'N/A'}"\n`;
                            debugText += `     text: "${el.textContent.trim().slice(0, 50)}"\n`;
                        });
                    }
                    debugText += '\n';
                } catch (e) {
                    debugText += `${selector}: ERROR: ${e.message}\n\n`;
                }
            });

            debugText += `\nPage Info:\n`;
            debugText += `URL: ${window.location.href}\n`;
            debugText += `Title: ${document.title}\n`;
            debugText += `Friends in backup: ${getBackupCount()}\n`;

            debugInfo.textContent = debugText;
        }
    }

    //
    // ─── BACKUP UTILITIES ─────────────────────────────────────────────────────────────────
    //

    function getBackupCount() {
        try {
            const backup = GM_getValue(BACKUP_KEY);
            return backup ? JSON.parse(backup).length : 0;
        } catch {
            return 0;
        }
    }

    //
    // ─── ROBLOX API UTILITIES ──────────────────────────────────────────────────────────────
    //

    // 1) Get the authenticated user’s ID by calling /users/authenticated
    async function getCurrentUserId() {
        let retries = 0;
        while (retries < MAX_RETRIES) {
            try {
                const resp = await fetch('https://users.roblox.com/v1/users/authenticated', {
                    method: 'GET',
                    credentials: 'include',
                    headers: { 'Accept': 'application/json' }
                });
                if (!resp.ok) throw new Error(`Status ${resp.status}`);
                const data = await resp.json();
                if (data && data.id) return data.id.toString();
                throw new Error('No ID in response');
            } catch (e) {
                retries++;
                console.warn(`getCurrentUserId attempt ${retries} failed: ${e.message}`);
                await new Promise(res => setTimeout(res, 1000));
            }
        }
        throw new Error('Failed to get current user ID after retries');
    }

    // 2) Use Roblox Friends API to fetch ALL friends (paginated)
    async function getAllFriends() {
        showNotification('Fetching full friend list...');
        try {
            const userId = await getCurrentUserId();
            let allFriends = [];
            let cursor = '';
            do {
                const url = new URL(`https://friends.roblox.com/v1/users/${userId}/friends`);
                url.searchParams.set('limit', '100');
                if (cursor) url.searchParams.set('cursor', cursor);

                const resp = await fetch(url.toString(), {
                    method: 'GET',
                    credentials: 'include',
                    headers: { 'Accept': 'application/json' }
                });
                if (!resp.ok) throw new Error(`Status ${resp.status}`);
                const data = await resp.json();
                if (Array.isArray(data.data)) {
                    data.data.forEach(f => {
                        // f is {id: <number>, name: <string>, ...}
                        allFriends.push({
                            userId: f.id.toString(),
                            username: f.name
                        });
                    });
                }
                cursor = data.nextPageCursor || '';
            } while (cursor);

            showNotification(`Detected ${allFriends.length} friends.`);
            return allFriends;
        } catch (err) {
            console.error('Error in getAllFriends():', err);
            showNotification('Failed to load friend list. Are you on a Roblox page while signed in?', true);
            return [];
        }
    }

    // 3) Get a fresh CSRF token by making a dummy POST request (Roblox standard)
    async function getCsrfToken() {
        try {
            const response = await fetch('https://friends.roblox.com/v1/users/1/request-friendship', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                credentials: 'include',
                body: JSON.stringify({})
            });
            const csrfToken = response.headers.get('x-csrf-token');
            if (!csrfToken) throw new Error('No X-CSRF-TOKEN in headers');
            return csrfToken;
        } catch (error) {
            console.error('Error getting CSRF token:', error);
            showNotification('Could not obtain CSRF token. Refresh the page and try again.', true);
            throw error;
        }
    }

    // 4) Unfriend a user via API
    async function unfriendUser(userId, username, csrfToken) {
        try {
            const resp = await fetch(`https://friends.roblox.com/v1/users/${userId}/unfriend`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': csrfToken
                },
                credentials: 'include'
            });
            if (resp.ok) {
                console.log(`✓ Unfriended ${username} (ID: ${userId})`);
                return true;
            } else {
                const errText = await resp.text();
                console.error(`✗ Failed to unfriend ${username}: ${resp.status} – ${errText}`);
                return false;
            }
        } catch (e) {
            console.error(`✗ Error unfriending ${username}:`, e);
            return false;
        }
    }

    // 5) Send a friend request via API
    async function friendUser(userId, username, csrfToken) {
        try {
            const resp = await fetch(`https://friends.roblox.com/v1/users/${userId}/request-friendship`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': csrfToken
                },
                credentials: 'include',
                body: JSON.stringify({ friendshipOriginSourceType: 1 })
            });
            if (resp.ok) {
                console.log(`✓ Friend request sent to ${username} (ID: ${userId})`);
                return true;
            } else {
                const errText = await resp.text();
                console.error(`✗ Failed to send friend request to ${username}: ${resp.status} – ${errText}`);
                return false;
            }
        } catch (e) {
            console.error(`✗ Error sending request to ${username}:`, e);
            return false;
        }
    }

    //
    // ─── NOTIFICATION ───────────────────────────────────────────────────────────────────
    //

    function showNotification(message, isError = false) {
        console.log(`${isError ? 'ERROR' : 'INFO'}: ${message}`);
        if (typeof GM_notification !== 'undefined') {
            GM_notification({
                text: message,
                title: 'Roblox Unfriend Tool',
                highlight: isError
            });
        } else {
            alert(message);
        }
    }

    //
    // ─── MASS UNFRIEND ───────────────────────────────────────────────────────────────────
    //

    async function unfriendAll() {
        const ui = getUIElements();
        if (!ui.unfriendAllBtn) return;

        const confirmMsg = 
            '⚠️ WARNING: This will unfriend ALL your friends!\n\n' +
            'A backup will be saved so you can recover them later.\n\n' +
            'Are you absolutely sure you want to continue?';
        if (!confirm(confirmMsg)) {
            return;
        }

        // 1) Fetch every friend via API:
        const friends = await getAllFriends();
        if (friends.length === 0) {
            return;
        }

        // 2) Save a backup (array of {username, userId})
        GM_setValue(BACKUP_KEY, JSON.stringify(friends));
        console.log(`Backup saved: ${friends.length} entries.`);

        try {
            ui.unfriendAllBtn.disabled    = true;
            ui.recoverFriendsBtn.disabled = true;
            ui.progressContainer.style.display = 'block';
            ui.statusText.textContent = 'Preparing to unfriend…';

            const csrfToken = await getCsrfToken();
            let successCount = 0;
            let failedCount  = 0;

            for (let i = 0; i < friends.length; i++) {
                const { userId, username } = friends[i];
                const pct = Math.round(((i + 1) / friends.length) * 100);
                ui.progressBar.style.width = `${pct}%`;
                ui.statusText.textContent = `Unfriending ${username} (${i+1}/${friends.length})…`;

                const ok = await unfriendUser(userId, username, csrfToken);
                if (ok) {
                    successCount++;
                } else {
                    failedCount++;
                }

                await new Promise(res => setTimeout(res, DELAY_BETWEEN_REQUESTS));
            }

            ui.progressBar.style.width = '100%';
            ui.statusText.textContent =
                `✅ Completed! Unfriended ${successCount}/${friends.length} friends. ` +
                `${failedCount} failed.`;
            showNotification(
                `Process completed! Unfriended ${successCount} out of ${friends.length} friends.`
            );

        } catch (err) {
            console.error('Error during mass unfriend:', err);
            ui.statusText.textContent = 'Error occurred. See console.';
            showNotification('Error during mass unfriend. Check console.', true);
        } finally {
            ui.unfriendAllBtn.disabled    = false;
            ui.recoverFriendsBtn.disabled = false;
        }
    }

    //
    // ─── RECOVER FRIENDS ─────────────────────────────────────────────────────────────────
    //

    async function showRecoverySelection() {
        const ui = getUIElements();
        if (!ui.friendSelection) return;

        const backupRaw = GM_getValue(BACKUP_KEY);
        if (!backupRaw) {
            showNotification('No backup found. Run “Unfriend All” first.', true);
            return;
        }

        let friends;
        try {
            friends = JSON.parse(backupRaw);
        } catch {
            showNotification('Could not parse backup data.', true);
            return;
        }
        if (!Array.isArray(friends) || friends.length === 0) {
            showNotification('Backup is empty.', true);
            return;
        }

        ui.friendsList.innerHTML = '';
        friends.forEach(f => {
            const item = document.createElement('div');
            item.className = 'friend-item';
            item.innerHTML = `
                <input type="checkbox" class="friend-checkbox" id="friend-${f.userId}" checked>
                <label class="friend-username" for="friend-${f.userId}">${f.username}</label>
            `;
            ui.friendsList.appendChild(item);
        });

        ui.selectAllCheckbox.checked = true;
        ui.friendSelection.style.display = 'block';
        showNotification(`Loaded ${friends.length} friends from backup.`);
    }

    async function recoverSelectedFriends() {
        const ui = getUIElements();
        if (!ui.friendsList) return;

        const checkedBoxes = ui.friendsList.querySelectorAll('.friend-checkbox:checked');
        if (checkedBoxes.length === 0) {
            showNotification('No friends selected for recovery.', true);
            return;
        }
        if (!confirm(`Send friend requests to ${checkedBoxes.length} selected friends?`)) {
            return;
        }

        let backup;
        try {
            backup = JSON.parse(GM_getValue(BACKUP_KEY));
        } catch {
            showNotification('Backup data is corrupted.', true);
            return;
        }

        const selectedIds = Array.from(checkedBoxes).map(cb => cb.id.replace('friend-', ''));
        const toRecover = backup.filter(f => selectedIds.includes(f.userId));

        try {
            ui.confirmRecoverBtn.disabled = true;
            ui.progressContainer.style.display = 'block';
            ui.statusText.textContent = 'Preparing recovery…';

            const csrfToken = await getCsrfToken();
            let successCount = 0;
            let failedCount  = 0;

            for (let i = 0; i < toRecover.length; i++) {
                const { userId, username } = toRecover[i];
                const pct = Math.round(((i + 1) / toRecover.length) * 100);
                ui.progressBar.style.width = `${pct}%`;
                ui.statusText.textContent = `Sending request to ${username} (${i+1}/${toRecover.length})…`;

                const ok = await friendUser(userId, username, csrfToken);
                if (ok) {
                    successCount++;
                } else {
                    failedCount++;
                }
                await new Promise(res => setTimeout(res, DELAY_BETWEEN_REQUESTS));
            }

            ui.progressBar.style.width = '100%';
            ui.statusText.textContent =
                `✅ Recovery done: sent ${successCount}/${toRecover.length} requests. ` +
                `${failedCount} failed.`;
            showNotification(
                `Recovery finished! Sent ${successCount} out of ${toRecover.length} requests.`
            );

            ui.friendSelection.style.display = 'none';
        } catch (err) {
            console.error('Error during friend recovery:', err);
            ui.statusText.textContent = 'Error during recovery. See console.';
            showNotification('Error during friend recovery. Check console.', true);
        } finally {
            ui.confirmRecoverBtn.disabled = false;
        }
    }

    //
    // ─── OPTIONAL “SCAN FRIENDS” ─────────────────────────────────────────────────────────
    //

    // This now simply calls getAllFriends() to check count and notify.
    async function scanFriends() {
        const ui = getUIElements();
        if (ui.scanBtn) ui.scanBtn.disabled = true;
        showNotification('Scanning for friends via API…');
        const friends = await getAllFriends();
        if (ui.scanBtn) ui.scanBtn.disabled = false;
        if (friends.length > 0) {
            showNotification(`Scan complete: found ${friends.length} friends.`);
        }
    }

    //
    // ─── EVENT BINDING ON LOAD ───────────────────────────────────────────────────────────
    //

    setTimeout(() => {
        const ui = getUIElements();

        if (ui.unfriendAllBtn) {
            ui.unfriendAllBtn.addEventListener('click', unfriendAll);
        }
        if (ui.recoverFriendsBtn) {
            ui.recoverFriendsBtn.addEventListener('click', showRecoverySelection);
        }
        if (ui.confirmRecoverBtn) {
            ui.confirmRecoverBtn.addEventListener('click', recoverSelectedFriends);
        }
        if (ui.debugBtn) {
            ui.debugBtn.addEventListener('click', showDebugInfo);
        }
        if (ui.scanBtn) {
            ui.scanBtn.addEventListener('click', scanFriends);
        }

        if (ui.selectAllCheckbox) {
            ui.selectAllCheckbox.addEventListener('change', function() {
                const allBoxes = ui.friendsList.querySelectorAll('.friend-checkbox');
                allBoxes.forEach(cb => cb.checked = this.checked);
            });
        }

        console.log('✅ Roblox Mass Unfriend Tool v3.1 loaded.');
        showNotification('Roblox Mass Unfriend Tool loaded! Click “Scan Friends” to test.');
    }, 1500);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址