Roblox Mass Unfriend with Recovery (Fixed v3.1)

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();