quick toggles

quick toggles for the main rblx page.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         quick toggles
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  quick toggles for the main rblx page.
// @homepage     https://lachlanm05.com
// @author       lachlanm05
// @match        https://www.roblox.com/home*
// @grant        none
// @license      MIT
// ==/UserScript==

// works like you'd expect.
// confused on how it works? just chuck it into any ai chatbot
// and it'll explain it better than anything i could write.

(function() {
    'use strict';

    // grab the initial security token from the page's meta tags. 
    // roblox requires this token to prove the request is coming from a real browser.
    function getInitialCsrfToken() {
        const tokenMeta = document.querySelector('meta[name="csrf-token"]');
        return tokenMeta ? tokenMeta.getAttribute('data-token') : '';
    }

    // global variables to track the active token and prevent the UI from fighting the user
    let activeCsrfToken = getInitialCsrfToken();
    let isUpdating = false;

    // this reads
    async function syncSettingsUI() {
        // if the user just clicked a switch, pause syncing so the boxes don't rubber-band.
        if (isUpdating) return;

        try {
            // we append a timestamp to the URL to completely defeat the browser cache,
            // forcing it to fetch the freshest data every single time.
            const bypassCacheUrl = 'https://apis.roblox.com/user-settings-api/v1/user-settings/settings-and-options?_=' + Date.now();
            const response = await fetch(bypassCacheUrl, {
                method: 'GET',
                credentials: 'include',
                headers: {
                    'Cache-Control': 'no-store, no-cache, must-revalidate',
                    'Pragma': 'no-cache'
                }
            });

            if (response.ok) {
                const data = await response.json();
                
                // Read the hidden API format (data.setting.currentValue) and update UI
                const onlineToggle = document.getElementById('toggle-online');
                if (onlineToggle && data.whoCanSeeMyOnlineStatus) {
                    onlineToggle.checked = (data.whoCanSeeMyOnlineStatus.currentValue === 'AllUsers');
                }

                const expToggle = document.getElementById('toggle-experience');
                if (expToggle && data.whoCanJoinMeInExperiences) {
                    expToggle.checked = (data.whoCanJoinMeInExperiences.currentValue === 'Followers');
                }
            } else {
                console.warn('Roblox Privacy Toggles: failed to sync. sorry. status: ' + response.status);
            }
        } catch (error) {
            console.error('Roblox Privacy Toggles: error fetching settings. status: ', error);
        }
    }

    // we write.
    // sends the new stuff.
    async function updatePrivacySetting(settingKey, settingValue) {
        if (!activeCsrfToken) {
            console.error('Roblox Privacy Toggles: couldnt find initial CSRF token');
            return;
        }

        // lock the UI so the 5-second polling doesn't overwrite our visual changes
        isUpdating = true;
        const payload = {};
        payload[settingKey] = settingValue;

        // wrapper for the fetch request so we can easily retry it if the token is stale
        async function makeRequest() {
            return fetch('https://apis.roblox.com/user-settings-api/v1/user-settings', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json;charset=utf-8',
                    'X-CSRF-TOKEN': activeCsrfToken
                },
                credentials: 'include', // ensures your .ROBLOSECURITY cookie is sent
                body: JSON.stringify(payload)
            });
        }

        try {
            let response = await makeRequest();

            // we shake hands :3
            // thankfully in a 403, we do actually get a new one.
            // then we can go take it and use it.
            if (!response.ok && response.status === 403 && response.headers.has('x-csrf-token')) {
                activeCsrfToken = response.headers.get('x-csrf-token');
                response = await makeRequest();
            }

            if (response.ok) {
                // very cool.
                // then unlock the UI and force a fresh read to confirm everything is synced.
                setTimeout(() => {
                    isUpdating = false;
                    syncSettingsUI();
                }, 1500);
            } else {
                // unlock if the thing fails
                isUpdating = false;
            }
        } catch (error) {
            isUpdating = false;
        }
    }

    // build
    function injectUI() {
        // thanks ai for telling me how to write html in js in a userscript.
        // along with css. practically how html and css work in js in a userscript.
        // gemini said that just making a function with const container and const htmlElements
        // would work. wow, how cool! /s
        // eh, it's better than vibe coding this function. human powered slop ftw.
        // all terrible ui colors and design was chosen by me.
        // thanks gemini-sensei.
      
      
        // create main floating box
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.bottom = '20px';
        container.style.right = '20px';
        container.style.backgroundColor = '#232527';
        container.style.padding = '15px';
        container.style.borderRadius = '8px';
        container.style.boxShadow = '0 4px 6px rgba(0,0,0,0.5)';
        container.style.color = '#fff';
        container.style.zIndex = '9999';
        container.style.fontFamily = 'Gotham, "Helvetica Neue", Helvetica, Arial, sans-serif';

        // we make html elements.
        const htmlElements = [
            '<div style="font-weight: bold; margin-bottom: 12px; font-size: 14px; text-align: center; border-bottom: 1px solid #393b3d; padding-bottom: 5px;">quick</div>',
            '<div style="display: flex; align-items: center; margin-bottom: 10px;">',
                '<input type="checkbox" id="toggle-online" style="margin-right: 8px; cursor: pointer;">',
                '<label for="toggle-online" style="font-size: 13px; cursor: pointer; user-select: none;">Show Online Status</label>',
            '</div>',
            '<div style="display: flex; align-items: center;">',
                '<input type="checkbox" id="toggle-experience" style="margin-right: 8px; cursor: pointer;">',
                '<label for="toggle-experience" style="font-size: 13px; cursor: pointer; user-select: none;">Show Current Experience</label>',
            '</div>'
        ];
        
        container.innerHTML = htmlElements.join('');
        document.body.appendChild(container);

        // event
        // update on click
        document.getElementById('toggle-online').addEventListener('change', (e) => {
            const value = e.target.checked ? 'AllUsers' : 'NoOne';
            updatePrivacySetting('whoCanSeeMyOnlineStatus', value);
        });

        document.getElementById('toggle-experience').addEventListener('change', (e) => {
            const value = e.target.checked ? 'Followers' : 'NoOne';
            updatePrivacySetting('whoCanJoinMeInExperiences', value);
        });

        // run an init scan then every 10 seconds
        syncSettingsUI();
        setInterval(syncSettingsUI, 10000); // this is ms, future me.
    }

    // wait for dear react to load.
    setTimeout(injectUI, 1500);

})();