Chat Module Admin Commands

I was toying around with the Quick-Admin Command Script I found in The Roblox Developer Hub.

They also provided a Permissions Module where I could restrict the Admin Commands to certain players or certain ranks in a group, but I’m having trouble figuring out how to set the admin commands for certain ranks in my group only. (They have a setting to do that).

There aren’t any errors in the output either.

They weren’t very clear on the tutorial on how to specifically set group ranks for the commands, so I need help on how to.

I’ve tried to edit the scripts a few times but the end result is always “ValiantWind is not an admin” when I try to play test in Studio.

I will provide the original Permissions Module and the edits I’ve tried so far:

Original Module Script:

local Permission = {}

local Admins = {
	--[UserId] = permission
}
local Groups = {
--	[GroupId] = {
	--	[GroupRank] = permission
	--}
}

--=======================================================================================================================================--
-- Permission
--  Properties
-- 		.Admins: table
-- 		.Groups: table
--	Functions
-- 		:GetAdmins() -> bool
-- 		:ContainsAdmin(number userId) -> bool
-- 		:SetUserPermission(number userId, number permission) -> bool
-- 		:GetUserPermission(number userId) -> number
--		:GetGroups() -> table
--		:ContainsGroupRank(number groupId, number rankId) -> bool
--		:SetGroupRankPermission(number groupId, number rankId, number permission)
--		:GetGroupRankPermission(number groupId, number rankId)
--		:GetUserGroupRankPermission(number userId)
--=======================================================================================================================================--
--=======================================================================================================================================--

--=======================================================================================================================================--
--=======================================================================================================================================--
-- UserId based admin functions
--=======================================================================================================================================--
--=======================================================================================================================================--

-- Returns the Admins table
function Permission:GetAdmins()
	return Admins
end

-- Returns whether or not a given userId exists in Admins
local function ContainsAdmin(userId)
	if not tonumber(userId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(userId), "number")) end
	userId = tonumber(userId)
	
	local isAdmin = Admins[userId]
	if not isAdmin then return false end
	
	if tonumber(isAdmin) then return true end -- True: user found in Admin table and is a numericalranking ? False: user not found in Admin table or found and not a number
	
	error("Stored permission level is an invalid type")	
	return false
end

-- Sets the permission level for a given userId in Admins
function Permission:SetUserPermission(userId, permission)
	if not tonumber(userId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(userId), "number")) end
	if not tonumber(permission) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(permission), "number")) end
	permission = tonumber(permission)
	
	
	local success = (ContainsAdmin(userId) and Admins[userId] == permission)
		
	if not success then
		Admins[userId] = permission

		success = (ContainsAdmin(userId) and Admins[userId] == permission)
	end
	return success
end	

-- Returns the permission level for a given userId in Admins
function Permission:GetUserPermission(userId)	
	if not tonumber(userId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(userId), "number")) end
	
	local isAdmin = ContainsAdmin(userId)
	if not isAdmin then return false end
	return Admins[userId]
end

--=======================================================================================================================================--
--=======================================================================================================================================--
-- GroupId/RankId based admin functions
--=======================================================================================================================================--
--=======================================================================================================================================--

-- Returns the Groups table
function Permission:GetGroups()
	return Groups
end

-- Returns whether or not a given groupId->rankId exists in Groups
local function ContainsGroupRank(groupId, rankId)
	if not tonumber(groupId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(groupId), "number")) end
	groupId = tonumber(groupId)
	if not tonumber(rankId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(rankId), "number")) end
	rankId = tonumber(rankId)
	
	local isGroup = Groups[groupId]
	if not isGroup then return false end
	
	local isRank = Groups[groupId][rankId]
	if not isRank then return false end
	
	if tonumber(isRank) then return true end	
	
	error("Stored permission level is an invalid type")	
	return false
end

-- Sets the permission level for a given groupId->rankId in Groups
function Permission:SetGroupRankPermission(groupId, rankId, permission)
	if not tonumber(groupId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(groupId), "number")) end
	groupId = tonumber(groupId)
	if not tonumber(rankId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(rankId), "number")) end
	rankId = tonumber(rankId)
	if not tonumber(permission) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(permission), "number")) end
	permission = tonumber(permission)
	
	if not tonumber(groupId) then return false end
	local success = (ContainsGroupRank(groupId, rankId) and Groups[groupId][rankId] == permission)
		
	if not success then
		if not Groups[groupId] then
			Groups[groupId] = {
				[rankId] = permission				
			}	
		else 
			Groups[groupId][rankId] = permission
		end
		
		success = (ContainsGroupRank(groupId, rankId) and Groups[groupId][rankId] == permission)
	end
	return success
end	

-- Returns the permission level for a given groupId->rankId in Groups
function Permission:GetGroupRankPermission(groupId, rankId)
	if not tonumber(groupId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(groupId), "number")) end
	groupId = tonumber(groupId)
	if not tonumber(rankId) then error(string.format("Incorrect value: given '%s' when expecting '%s'.", typeof(rankId), "number")) end
	rankId = tonumber(rankId)
	
	local isGroupRank = ContainsGroupRank(groupId, rankId)
	
	if isGroupRank then return Groups[groupId][rankId] end
	return nil
end

-- Returns the highest permission level for a given userId in Groups for all of that user's groups
function Permission:GetUserGroupRankPermission(userId)
	local username = game.Players:GetNameFromUserIdAsync(userId)
	local player = game.Players:FindFirstChild(username)
	if not player then return false end
	
	local highestPermission = 0	
	
	for groupId, groups in pairs(Groups) do
		local isInGroup = player:IsInGroup(groupId)
		if isInGroup then
			local rankId = player:GetRankInGroup(groupId)
			local hasRank = ContainsGroupRank(groupId, rankId)
			
			if hasRank then
				local groupRankPermission = Groups[groupId][rankId]
				
				if groupRankPermission > highestPermission then highestPermission = groupRankPermission end
			end
		end
	end
	
	return highestPermission
end

return Permission

Here’s the edits I’ve done for the Group Table at the top that sets the Commands’ permissions:
(Please note that I’ve tried using numbers besides zero for setting the permissions)

Edit 1:

local Groups = {
	[13622916] = { -- My Group ID
	[255] = 0,
    [254] = 0,
    [253] = 0
	}
}

Edit 2:

local Groups = {
	[13622916] = { -- My Group ID
	[255] = "0"
    [254] = "0"
    [253] = "0"
	}
}

Edit 3:

local Groups = {
	[13622916] = { -- My Group ID
	[255] = 0
	}
}
local Groups = {
	[13622916] = { -- My Group ID
	[255] = 0
	}
}

Pinging Chat Scripting Expert @colbert2677 for help

I’ve never used any of the chat resources Roblox Resources have created since I typically write my own modules but you can check the flow of logic here and determine how the modules are using each other.

Quick Start Admin Commands requires and sets up Admin Commands Module, the latter of which implements Permission Module. If you’re using QSAC then the other two modules are pulled from the website and therefore are working with defaults. Naturally if you insert the permissions module it wouldn’t do anything because it’s not the module the main commands module requires and implements as the permission system.

You will either need to fork these modules and change the require paths to the copies you fork or call the Public API to add your own set of permissions.

Forking

If you are forking then insert all three modules in the following manner:

image

  • InsertDefaultModules is a BoolValue that has a Value of true; it is required
  • AdminCommands (QSAC Model) goes in ChatModules
  • Admin Commands Module is renamed to PublicAPI and goes in AdminCommands
  • Permission Module is renamed to PermissionModule and goes in PublicAPI

In AdminCommands uncomment the top line and remove the id-require:

- --local PublicAPI = require(script.PublicAPI)
+ local PublicAPI = require(script.PublicAPI)
- local PublicAPI = require(1163352238)

In CommandsSystem, replace line 10’s require with a relative require:

- local Permission = require(1163351226)
+ local Permission = require(script.Parent.PermissionModule)

You can then modify the Groups table of PermissionModule to implement your group ranks.

local Groups = {
    [13622916] = {
        [255] = math.huge,
        [254] = 999,
        -- etc.
    },
}

Not Forking

QSAC adds Admin Commands Module as the local variable PublicAPI, though because QSAC itself returns a function you need to do your setup in the QSAC module itself. The main idea is that you’re going to define all your user and/or group permissions then call the PublicAPI to set the permissions according to what you define.

In my opinion it’d be most readable if you defined your permissions after the DeveloperMode flag set, so after PublicAPI and Utilities are defined create your user and group tables. How you choose to set up your tables is up to you, but the main idea is having a way to define those tables at all.

local PublicAPI = require(1163352238)
local Utilities = PublicAPI.Utilities

PublicAPI.Commands.DeveloperMode = false

+ local Admins = {}
+ local Groups = {}

You should write these in the same format documented in the Permissions module. From there, you can either do this after all the BindCommands and right before the module returns the Run function or alongside the local functions created after the API declarations but you’ll need to go through your user and groups tables and set their permissions via the PublicAPI.

Specifically call SetPermission from the Commands index of the PublicAPI. Commands has permission functions that call directly to the permission module to update permissions while the PublicAPI’s version is specifically designed to be called with a requester and a target to update.

for userId, permissionLevel in pairs(Admins) do
    -- You're responsible for ensuring both the key and value are ids. You should
    -- not need to do any validation for this kind of configuration.
    PublicAPI.Commands:SetUserPermission(userId, permissionLevel)
end

for groupId, ranks in pairs(Groups) do
    -- Similarly to the above, key should be a number and value a table. Because
    -- of the structure of the group table, we need a for loop for each first
    -- dimension entry to set the levels of the second dimension.
	for rank, permissionLevel in pairs(ranks) do
		PublicAPI.Commands:SetGroupRankPermission(groupId, rank, permissionLevel)
	end
end

Obviously you should have a more readable sample, but a rough idea of how it could look when it’s done is below. Each line of code must be called in this order but otherwise it doesn’t matter where they go. For example, the for loops could be at the very bottom and the tables at the top but it should always appear in that order so that it works as intended.

Please do make sure you’ve learned something and ask questions if you don’t understand. It’s more important that I know I’ve been able to sufficiently teach the flow of code here and what’s going on rather than having people swipe the finished code without understanding what it does. Providing the finished code is a way to see it assembled, not to spoonfeed. Just have to disclaim that for developers who complain about copy-pasted work not working rather than using their newfound knowledge to attempt to debug it, provided it’s not my own explanation mistake.

--local PublicAPI = require(script.PublicAPI)
local PublicAPI = require(1163352238)
local Utilities = PublicAPI.Utilities

PublicAPI.Commands.DeveloperMode = false

local Admins = {}
local Groups = {
	[3451727] = {[254] = math.huge}
}

for userId, permissionLevel in pairs(Admins) do
	-- You're responsible for ensuring both the key and value are ids. You should
	-- not need to do any validation for this kind of configuration.
	PublicAPI.Commands:SetUserPermission(userId, permissionLevel)
end

for groupId, ranks in pairs(Groups) do
	for rank, permissionLevel in pairs(ranks) do
		PublicAPI.Commands:SetGroupRankPermission(groupId, rank, permissionLevel)
	end
end

image

3 Likes

Ahhh, I see. Those are the two biggest mistakes I was making.

Is it better to fork the Chat Paths in order to implement the permissions or is it better to add my own?

That makes sense and will help. Scripts always works best when they’re efficient, clean, and organized.

What the BoolValue does is self-explanatory right?

I don’t have any more questions right now, as I have only read what you said for now, but when I try using the method in Studio myself I’ll probably have more.

Anyway, thanks a lot! I appreciate your help. :slightly_smiling_face:

1 Like

It strictly depends. I don’t normally recommend forking if you’re using a public resource if your only intent is to modify configurations. If the resource provides an API that allows you to manage configurations then you should use that. Forking should really only be used in cases where it’s needed:

  • Configurations can’t be managed because there’s no developer-facing configuration APIs (which is poor technical design on the part of the resource developer)

  • You need to make deep changes to the code or structuring that you could otherwise not do if you were not forking (things that can’t be accomplished by using exported API)

In the case of these admin commands, QSAC’s purpose is to use the Admin Commands Module API to set up and create a default set of commands, as is implicated in the title “Quick Start Admin Commands”. QSAC is the equivalent of writing your own layer to integrate the Admin Commands Module and create your own commands and such. There’s no need to fork in this case and I’d personally recommend using the non-forking option.

That being said, forking is mostly discouraged for resources that may update often primarily because you won’t be able to pull any new code (technical, security or otherwise changes) once an update is made. This shifts update responsibility from automation to your own hands and depending on what kind of changes you’ve made on your own it can get understandably messy or difficult to track which updates made need to be implemented on your side. These resources don’t really update so it’s up to you whether or not you want to fork them.

Regarding InsertDefaultModules, yes it’s somewhat self explanatory. Since this module is designed to be called by the ChatService during setup you need to place this in a ChatModules folder in the Chat service. This folder being present however tells the engine not to inject a ChatModules folder so it may also not insert other default modules that Chat uses. InsertDefaultModules tells the engine that while a ChatModules folder exists, you also want the other modules that’d normally be there to be added.

2 Likes

Thank you for the clarification. I will follow you up with more questions once I try out your methods.