Roblox Role Dropdown

A simple tool that adds a dropdown menu to change the role of your group members directly from the group wall.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Roblox Role Dropdown
// @description  A simple tool that adds a dropdown menu to change the role of your group members directly from the group wall.
// @author       HotDog6400
// @copyright    Copyright (c) 2021, 2025 HotDog640
// @namespace    https://www.roblox.com/users/694999148/
// @license      MIT
// @version      0.9.6
// @match        https://*.roblox.com/groups/*
// @match        https://*.roblox.com/communities/*
// @grant        none
// ==/UserScript==

/* eslint-disable curly, no-multi-spaces, no-undef */
// @ts-check

// *******************
// * Editable config *
// *******************
const RRDConfig = {
    enabled: true,              // Set this to false if you want to disable RRD.
    dropdown_width: '12rem'     // The width of dropdown. Can be any CSS length.
};

// *********************************************************************
// * Don't edit beyond this point! (unless you know what you're doing) *
// *********************************************************************
if (
    window.location.href.match(/roblox\.com\/(groups|communities)\/\d/) && RRDConfig.enabled
) (async () => {
    'use strict';
    /** @typedef {{ id: number; name: string; rank: number; }[]} Roles */

    console.log('RRD: Starting Roblox Role Dropdown');

    const groupId = window.location.href.split('/')[4];

    /** @type {Roles} */
    const roles = (await $.ajax(`https://groups.roblox.com/v1/groups/${groupId}/roles`)).roles;
    const currentUser = await $.ajax(`https://groups.roblox.com/v1/groups/${groupId}/membership`);

    if (!roles || !currentUser)
        return console.error('RRD: Error: Could not load the required resources');
    if (currentUser.permissions.groupMembershipPermissions.changeRank === false)
        return console.log('RRD: Exiting: Authenticated user can\'t change roles in this group');

    ///////////////////////////////////////////////////////////////////////////

    let RBXAlertTimeout;

    /**
     * @param {string} message
     * @param {boolean} [success]
     */
    const RBXAlert = (message, success = true) => {
        const alertClasses = ['alert-warning', 'alert-success'];
        const alertBox = $('.alert')[0];
        const alertContent = $('.alert-content')[0];

        const resetAlert = () => {
            alertBox.classList.remove('on');
            alertBox.classList.add('alert-success');
        }
        resetAlert()

        clearTimeout(RBXAlertTimeout);

        setTimeout(() => {
            alertBox.classList.remove(alertClasses[success ? 0 : 1]);
            alertBox.classList.add(alertClasses[success ? 1 : 0]);
            alertBox.classList.add('on');
            alertContent.innerText = message;
            RBXAlertTimeout = setTimeout(resetAlert, 5000);
        }, 100);
    }

    /** Global function for the dropdowns to access
     * @param {HTMLAnchorElement} anchor
     * @param {number} userId
     * @param {number} roleId
     * @param {string} roleName
     */
    // @ts-ignore
    window.changeRole = async (anchor, userId, roleId, roleName) => {
        $.ajax({
            method:'PATCH',
            url: `https://groups.roblox.com/v1/groups/${groupId}/users/${userId}`,
            data: { roleId }
        }).done(() => {
            const button = $(anchor.parentElement.parentElement.parentElement).find('.input-dropdown-btn')[0];
            button.title = $(button).find('.rbx-selection-label')[0].innerHTML = roleName;
            RBXAlert('Successfully changed role.');
        }).fail(res => {
            RBXAlert(res.responseJSON.errors[0].userFacingMessage, false);
        });
    }

    const allowedRoles = roles.filter(role =>
        role.rank !== 0 && role.rank < currentUser.userRole.role.rank
    );

    /**
     * @param {string} currentRole
     * @param {string} userId
     * @param {boolean} disabled
     * @returns {ChildNode}
     */
    const generateDropdown = (currentRole, userId, disabled) => {
        const template = document.createElement('template');
        template.innerHTML =
            `<div class="input-group-btn">
                <button class="input-dropdown-btn" data-toggle="dropdown" title="${currentRole}" ${disabled ? 'disabled: "disabled"' : ''}>
                    <span class="rbx-selection-label">${currentRole}</span>
                    <span class="icon-down-16x16"></span>
                </button>
                <ul class="dropdown-menu" data-toggle="dropdown-menu" role="menu">
                    ${allowedRoles.map(role =>
                        `<li title="${role.name}">
                            <a onClick="changeRole(this, ${userId}, ${role.id}, '${role.name}')">
                                <span class="text-overflow">${role.name}</span>
                            </a>
                        </li>`
                    ).join('')}
                </ul>
            </div>`;
        return template.content.firstChild;
    }

    /**
     * @param {JQuery<Node>} $post
     */
    // TODO: Make the dropdown disabled if the target's role is the same or higher than the auth'd user
    const putDropdown = ($post) => {
        const userId = /** @type {HTMLAnchorElement} */ ($post.find(
            '.avatar-headshot .avatar-card-link'
        )[0]).href.split('/')[4];
        const footer = $post.find('.text-caption-medium')[0];
        const content = $post.find('.list-body .list-content')[0];

        const footerArr = footer.innerText.split('|');
        const date = footerArr.splice(footerArr.length - 2, 2).join('|').trim();
        const currentRole = footerArr.join('|').trim();

        // const currentRole = footer.innerText.split('|')[0].trim();

        const disabled = false;

        const dropdown = generateDropdown(currentRole, userId, disabled);

        content.style.margin = '0 0 6px';
        footer.style.display = 'inline-grid';
        footer.style.gridTemplateColumns = `${RRDConfig.dropdown_width} auto`;
        footer.style.gridColumnGap = '8px';
        footer.style.alignItems = 'center';

        footer.innerHTML = date;
        footer.prepend(dropdown);
    }

    /**
     * @param {Element} posts
     */
    const createObserver = (posts) => {
        const observer = new MutationObserver(mutationsList =>
            mutationsList.forEach(mutation => {
                const $post = $(mutation.addedNodes[0]);
                if ($post.hasClass('comment'))
                    putDropdown($post);
            })
        );
        observer.observe(posts, { childList: true });
    }

    const posts = document.querySelector('.group-comments');
    if (posts) createObserver(posts);
    else {
        // Wait for posts to exist
        const observer = new MutationObserver(() => {
            const posts = document.querySelector('.group-comments');
            if (posts) {
                createObserver(posts);
                observer.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }
})();