Roblox Role Dropdown

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

// ==UserScript==
// @name         Roblox Role Dropdown
// @namespace    https://www.roblox.com/users/694999148/
// @version      0.9.4
// @description  A simple tool that adds a dropdown menu to change the role of your group members directly from the group wall.
// @author       HotDog6400
// @match        https://*.roblox.com/groups/*
// @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\/\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('.list-body .text-date-hint')[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 });
    }
})();