Roblox Mass Appeal

Adds a button to appeal all appealable items with concurrent requests. Improved X-CSRF handling.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Roblox Mass Appeal
// @namespace    github.com/annaroblox
// @version      2.0
// @description  Adds a button to appeal all appealable items with concurrent requests. Improved X-CSRF handling.
// @author       AnnaRoblox
// @match        *://*.roblox.com/report-appeals*
// @match        *://*.roblox.com/*/report-appeals*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      apis.roblox.com
// @connect      auth.roblox.com
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    console.log("Roblox Mass Appeal [v2.0]: Script active.");

    const APPEAL_MESSAGE_PLACEHOLDER = "I believe this moderation was a mistake. I respectfully request a review. Thank you";

    let currentCsrfToken = null;

    function getMetaCsrf() {
        return document.querySelector('meta[name="csrf-token"]')?.getAttribute('data-token') || null;
    }

    async function fetchFreshCsrfToken() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: "https://auth.roblox.com/v2/logout",
                headers: { "Content-Type": "application/json" },
                withCredentials: true,
                onload: function (r) {
                    const token = r.responseHeaders.match(/x-csrf-token:\s*([^\r\n]+)/i)?.[1];
                    if (token) {
                        currentCsrfToken = token.trim();
                        resolve(currentCsrfToken);
                    } else {
                        reject("Could not extract token from headers");
                    }
                },
                onerror: () => reject("Network error while fetching token")
            });
        });
    }

    async function getValidToken() {
        let token = getMetaCsrf() || currentCsrfToken;
        if (token) return token;

        try {
            token = await fetchFreshCsrfToken();
            console.log("✅ Got fresh CSRF token");
            return token;
        } catch (e) {
            console.error("Failed to get token:", e);
            throw e;
        }
    }

    function addAppealUI(injectionPoint) {
        if (document.getElementById('mass-appeal-container')) return;

        const container = document.createElement('div');
        container.id = 'mass-appeal-container';
        container.innerHTML = `
            <h2>Mass Appeal Tool v2.0</h2>
            <p>Enter message and click the button. Now with auto token retry on 403.</p>
            <textarea id="mass-appeal-message" placeholder="Enter your appeal message here...">${APPEAL_MESSAGE_PLACEHOLDER}</textarea>
            <button id="mass-appeal-button">Appeal All Found Items</button>
            <div id="mass-appeal-status"></div>
        `;
        injectionPoint.prepend(container);

        GM_addStyle(`
            #mass-appeal-container { background:#2c2f33; color:#fff; padding:20px; margin:15px 0; border:1px solid #444; border-radius:8px; z-index:9999; }
            #mass-appeal-message { width:98%; height:110px; padding:10px; background:#23272a; color:#fff; border:1px solid #555; border-radius:4px; }
            #mass-appeal-button { background:#7289da; color:white; padding:12px 20px; border:none; border-radius:5px; cursor:pointer; font-size:16px; }
            #mass-appeal-button:hover { background:#677bc4; }
            #mass-appeal-button:disabled { background:#555; cursor:not-allowed; }
            #mass-appeal-status { margin-top:15px; padding:12px; background:#23272a; border-radius:4px; max-height:350px; overflow-y:auto; font-family:monospace; }
        `);

        document.getElementById('mass-appeal-button').addEventListener('click', handleAppealAll);
    }

    function logStatus(statusDiv, text, isError = false) {
        const p = document.createElement('p');
        p.textContent = text;
        p.style.color = isError ? '#f04747' : '#43b581';
        statusDiv.appendChild(p);
        statusDiv.scrollTop = statusDiv.scrollHeight;
    }

    async function sendAppealRequest(userId, violationId, message, csrfToken) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: `https://apis.roblox.com/moderation-appeal-service/v2/users/${userId}/appeals`,
                headers: {
                    "Content-Type": "application/json;charset=UTF-8",
                    "X-CSRF-TOKEN": csrfToken,
                    "Referer": "https://www.roblox.com/"
                },
                data: JSON.stringify({
                    appeal: {
                        violation: `users/${userId}/violations/${violationId}`,
                        message: message
                    }
                }),
                withCredentials: true,
                onload: function (r) {
                    if (r.status >= 200 && r.status < 300) {
                        resolve("Success");
                    } else if (r.status === 403) {
                        // Try to extract new token
                        const newToken = r.responseHeaders.match(/x-csrf-token:\s*([^\r\n]+)/i)?.[1];
                        if (newToken) currentCsrfToken = newToken.trim();
                        reject({status: 403, body: r.responseText, newToken});
                    } else {
                        reject({status: r.status, body: r.responseText || r.statusText});
                    }
                },
                onerror: () => reject({status: 0, body: "Network error"})
            });
        });
    }

    async function handleAppealAll() {
        const button = document.getElementById('mass-appeal-button');
        const statusDiv = document.getElementById('mass-appeal-status');
        const message = document.getElementById('mass-appeal-message').value.trim();

        button.disabled = true;
        button.textContent = 'Processing...';
        statusDiv.innerHTML = '';

        const log = (text, isError = false) => logStatus(statusDiv, text, isError);

        if (!message) {
            log("Message cannot be empty!", true);
            resetButton(); return;
        }

        const userId = getUserId();
        if (!userId) {
            log("Could not find User ID. Are you logged in?", true);
            resetButton(); return;
        }

        log(`User ID: ${userId}`);

        let csrfToken;
        try {
            csrfToken = await getValidToken();
            log("✅ CSRF token ready");
        } catch (e) {
            log("❌ Failed to get CSRF token", true);
            resetButton(); return;
        }

        const violationLinks = Array.from(document.querySelectorAll('a[href^="#/v/"]'));
        const violationIds = [...new Set(violationLinks.map(link => link.href.split('#/v/')[1]).filter(Boolean))];

        if (violationIds.length === 0) {
            log("No violations found on this page.", true);
            resetButton(); return;
        }

        log(`Found ${violationIds.length} violations. Sending...`);

        for (let i = 0; i < violationIds.length; i++) {
            const id = violationIds[i];
            let attempts = 0;
            const maxAttempts = 2;

            while (attempts < maxAttempts) {
                try {
                    await sendAppealRequest(userId, id, message, csrfToken);
                    log(`[${i+1}/${violationIds.length}] ✅ SUCCESS: ${id}`);
                    break;
                } catch (err) {
                    attempts++;
                    if (err.status === 403 && attempts < maxAttempts) {
                        log(`[${i+1}/${violationIds.length}] 403 - Refreshing token and retrying...`, false);
                        if (err.newToken) csrfToken = err.newToken;
                        else csrfToken = await getValidToken();
                        continue;
                    }
                    log(`[${i+1}/${violationIds.length}] ❌ FAILED: ${id} → ${err.body || err.status}`, true);
                    break;
                }
            }
        }

        log("✅ All done! Reloading in 5 seconds...");
        button.textContent = 'Finished!';
        setTimeout(() => location.reload(), 5000);
    }

    function getUserId() {
        const match = document.querySelector('a[href*="/users/"]')?.href.match(/\/users\/(\d+)/);
        return match ? match[1] : document.querySelector('meta[name="user-id"]')?.content || null;
    }

    function resetButton() {
        const btn = document.getElementById('mass-appeal-button');
        if (btn) {
            btn.disabled = false;
            btn.textContent = 'Appeal All Found Items';
        }
    }

    // UI Injection
    let attempts = 0;
    const interval = setInterval(() => {
        attempts++;
        const selectors = ['#report-appeals-app', 'div[role="main"]', '#container-main', '#content', 'body'];
        for (const sel of selectors) {
            const el = document.querySelector(sel);
            if (el && !document.getElementById('mass-appeal-container')) {
                addAppealUI(el);
                clearInterval(interval);
                return;
            }
        }
        if (attempts > 30) {
            clearInterval(interval);
            addAppealUI(document.body);
        }
    }, 600);
})();