Making chat admin commands using the Chat Service

Things you need to know before doing this tutorial

The things listed above will not be explained, as I expect you know them if you read on

Things this tutorial will teach you

  • How to use the ChatService:RegisterProcessCommandsFunction() function
  • How to make a command parsing function
  • How to find command targets

If you have anything to add or if you have any questions, please reply, I’ll try to get back to you as soon as possible

If you find any grammatical errors that I haven’t found yet please let me know


What is the Chat Service?

When you open studio and go into your explorer, you’ll see something called “SoundService” underneath that is another service called “Chat”. This is not the Chat Service, the Chat Service is inside of the Chat. You may realize that there are no objects inside of Chat. That’s because the Chat Service is automatically added to Chat when the game runs.

How do we access the Chat Service?

As I said above, the Chat Service is added to the Chat during a play test (or in a public or private server), so to begin this tutorial, initiate a play test. When you look in the explorer you will now see the Chat now has some objects inside of it.

ChatService_Tutorial

There are multiple different objects here, but all we need to focus on is the ChatServiceRunner and the ChatModules, but there’s no ChatServiceso where is it? The ChatService is located inside of the ChatServiceRunner along with some other module scripts.

Manipulating the Chat Service

To begin messing around with the ChatService you need to select the ChatModules folder and use the Ctrl + C shortcut to copy it. Once you have copied the ChatModules you may end the play test and paste the ChatModules into the Chat using the Ctrl + V shortcut. Putting this new folder into the Chat will overwrite the original. Create a new module script inside of the ChatModules so we can begin. Inside of the the module write the following code:

function Run(ChatService)
	--Hopefully you noticed that "ChatService" parameter
	--This is parameter is the same thing as requiring the Chat Service module
	--If it's the same, why are we doing this inside of the ChatModules?
	--We're doing it like this because all module scripts inside of the ChatModules
	--Are required and then the returned value of the module script is called
	--Since we're returning a function from this module, that function will be called and the ChatService parameter will be filled in automatically	
end

return Run

Now that we understand how we can require the ChatService, we can can start working with the ChatService:RegisterProcessCommandsFunction function.

function Run(ChatService)
	local function isCommand(speakerName, message, channelName)
		--The speaker name is the name of the speaker
		--The message is the message the speaker sent
		--The channel name is the name of the chat channel the message was sent on
		print(speakerName)
		print(message)
		print(channelName)
		if message:lower() == "testing" then
			return true
		end
		return false
		--When you say /e [name of emote] into the chat
		--It plays the emote and stops the message from being sent
		--That's what we're doing here
		--When this function returns true the message doesn't get sent, when it returns false it does get sent
	end

	ChatService:RegisterProcessCommandsFunction("cmd", isCommand)
	--This function calls and auto fills the parameters of the isCommand function we created
end

return Run

Now that we know how to let the ChatService know we have sent a command, we can now get to the fun part, making commands! :smiley:

The Fun Part

local commands = {}

We’re going to leave that table blank because we’ll be creating a function that adds commands to it

local commands = {}

-- [[ Services ]] --
local ServerScriptService = game:GetService("ServerScriptService")
local PlayersService = game:GetService("Players")

local Settings = {
	Prefix = ";"; -- Symbol that lets the script know the message is a command
	ChannelName = "Admin"; -- Name of the chat channel we'll be creating to send admin commands on
	DebugMode = false; -- Set to true when making new commands so it's easier to identify errors
	DefaultPerm = 0; -- If a player is not on the admins list, this is what level of permission they'll have
	Admins = {
		-- Dictionary of user ids and the rank the player with that user id will be receiving (rank must be a number)
		[game.CreatorId] = math.huge;
		[0] = 1;
	}
}

local DataStore2 = require(1936396537)
-- *** MAKE SURE YOU HAVE THE DATASTORE2 MODULE IN YOUR GAME *** --

function SendMessageToClient(data, speakerName)
	local ChatService = require(ServerScriptService.ChatServiceRunner.ChatService)
	-- The ChatService can also be found in the ServerScriptService
	local Speaker = ChatService:GetSpeaker(speakerName)
	-- The speaker is another module script in the ChatServiceRunner that has functions related to the speaker and some other things
	local extraData = {data.ChatColor} -- Sets the color of the message
	Speaker:SendSystemMessage(data.Text, Settings.ChannelName, extraData)
	-- Sends a private message to the speaker
end
-- Function to send error messages to the person who attempted to use the command

function GetSpeakerPerms(speakerName)
	local playerObj = PlayersService[speakerName] -- Finds the actual player object
	local permission = DataStore2("perms", playerObj):Get(Settings.DefaultPerm)
	return permission
end
--Gets the permission level of the speaker
function SetSpeakerPerms(speakerName, level)
	local playerObj = PlayersService[speakerName]
	local permission = DataStore2("perms", playerObj)
	permission:Set(tonumber(level))
end
--Sets the permission level of the speaker
function StartSpeaker(speakerName)
	local playerObj = PlayersService[speakerName]
	local perms = GetSpeakerPerms(speakerName)
	
	if perms <= -1 then
		playerObj:Kick("You are banned from this place")
	elseif Settings.Admins[playerObj.UserId]then
		SetSpeakerPerms(speakerName,Settings.Admins[playerObj.UserId])
	end
	
end
function GetTarget(player, msg)
	local msgl = msg:lower()
	local ts = {} -- Targets table
	
	if msgl == "all" then
		--// Loop through all players and add them to the targets table
		for i, v in pairs(PlayersService:GetPlayers()) do
			table.insert(ts, v)
		end
	elseif msgl == "others" then
		--// Loops through all players and only adds them to the targets table if they aren't the player
		for i, v in pairs(PlayersService:GetPlayers()) do
			if v.Name ~= player.Name then
				table.insert(ts, v)
			end
		end
	elseif msgl == "me" then
		--// Loops through all players and only adds them to the targets table if they are the player
		for i, v in pairs(PlayersService:GetPlayers()) do
			if v.Name == player.Name then
				table.insert(ts, v)
			end
		end
	else
		--// Loops through all players and only adds them to the targets table if a portion of their name was in the msg
		-- for example
		-- if i wanted to admin somebody named "Jerry"
		-- I could say
		-- ;admin jer
		for i, v in pairs(PlayersService:GetPlayers()) do
			if v.Name:lower():sub(1, #msgl) == msg:lower() then
				table.insert(ts, v)
			end
		end
	end
	return ts
end
-- If speaker is on the admins list this will admin them, if they're banned this will ban them
function BindCommand(data)
	commands[data.name] = data
end
-- Adds a new command to the commands table
function BindCommands()
	
	-- For this tutorial I'll just be making a simple kill command, you can of course make more
	BindCommand({name = "kill", perm = 1, func = function(speaker, args)
		local commandTargets = GetTarget(speaker, args[1] or "me")
		
		if #commandTargets == 0 then
			-- No target was specifie so we can't do anything
			SendMessageToClient({
				Text = "No targets specified";
				ChatColor = Color3.new(1, 0, 0)
			}, speaker.Name)
			return -- End command
		end
		
		for _, target in pairs(commandTargets) do
			-- Loop through targets table
			local targetPerm = GetSpeakerPerms(target.Name)
			local speakerPerm = GetSpeakerPerms(speaker.Name)
			if targetPerm >= speakerPerm and speaker.Name ~= target.Name then
				-- Since a kill command can be seen as abusive to use
				-- I made it so people of lower ranks can't use it on higher ranks or people of the same rank
				SendMessageToClient({
					Text = "You cannot use this command on this player";
					ChatColor = Color3.new(1, 0, 0)
				}, speaker.Name)
				return -- End command
			end
			if target.Character then
				target.Character:BreakJoints() -- Kill
			end
		end
	end})
	
	--[[
		Make sure your table looks something like this
		{
			name = "command_name",
			perm = number,
			func = function(speaker, args)
				-- random code
			end
		}
	]]
end
-- Binds all commands at once
function Run(ChatService)
	
	spawn(BindCommands) -- Bind all the commands
	
	local AdminChannel = ChatService:AddChannel(Settings.ChannelName)
	-- Add a new channel for admin commands
	AdminChannel.WelcomeMessage = ""
	AdminChannel.AutoJoin = true
	-- Make all speakers join the channel by default
	
	AdminChannel.SpeakerJoined:Connect(StartSpeaker)
	-- When a new speaker is added to the channel it calls the StartSpeaker function
	
	local function ParseCommand(speakerName, message, channelName)
		local isCommand = message:match("^"..Settings.Prefix)
		-- Pattern that returns true if the prefix starts off the message
		if isCommand then
			local speaker = ChatService:GetSpeaker(speakerName) -- Requires the speaker module from the speaker module in the ChatServiceRunner
			local perms = GetSpeakerPerms(speakerName) -- Get speaker's permission level
			
			local messageWithoutPrefix = message:sub(#Settings.Prefix+1,#message) -- Get all characters after the prefix
			local command = nil -- The command the player is trying to execute (we haven't found that yet)
			local args = {} -- Table of arguments
			-- Arguments are words after the command
			-- So let's say the command was 
			-- ;fly jerry
			-- jerry would be the 1st argument
			for word in messageWithoutPrefix:gmatch("[%w%p]+") do
				-- Loops through a table of words inside of the message
				if command ~= nil then
					table.insert(args, word)
				else
					command = word:lower()
				end
			end
			-- Identify the command and get the arguments
			local properCommand = command:sub(1,1):upper() .. command:sub(2,#command):lower()
			-- This converts something like "fLy" into "Fly"
			if commands[command] then
				-- Command exists
				local commandPerm = commands[command].perm
				if commandPerm > perms then
					-- Player does not have permission to use this command
					SendMessageToClient({
						Text = "You do not have access to this command";
						ChatColor = Color3.new(1, .5, 0)
					}, speakerName)
					return false
				else
					-- Player has access to the command
					if Settings.DebugMode then
						-- Only shows output of command when DebugMode is on
						-- I'd turn it on if you're creating new commands and need to test them
						local executed, err = pcall(function()
							return commands[command].func(PlayersService[speakerName], args)
						end)
						if executed then
							SendMessageToClient({
								Text = "\"" .. properCommand .. "\" ran successfully";
								ChatColor = Color3.new(0, 1, 0)
							}, speakerName)
							return true
						else
							-- An error accured when executing
							SendMessageToClient({
								Text = "\"" .. properCommand .. "\" experienced an issue: " .. err;
								ChatColor = Color3.new(1, 0, 0)
							})
							return false
						end
					else
						-- DebugMode is disabled so we just execute the command
						pcall(commands[command].func, PlayersService[speakerName], args)
						return true
					end
				end
			else
				-- Command doesn't exist
				SendMessageToClient({
					Text = "\"" .. properCommand .. "\" doesn't exist!";
					ChatColor = Color3.new(1, 0, 0)
				}, speakerName)
				return false
			end
		end
		return false
	end
	
	ChatService:RegisterProcessCommandsFunction("cmd", ParseCommand)
end

return Run

I may do more tutorials like this in the future and create a module for this

There’s more about the Lua Chat System here → Lua Chat System if you’re interested

19 Likes

Nice, Helpful for new devs like me

3 Likes

I usually use player.Chatted for my commands. I will take this new method into consideration.

4 Likes

I mean, it’s alright. However, it requires you to fork the chat.
As @TwyPlasma said player.Chatted might be a better method if you want to keep up to date with the current roblox chat system.


Some Feedback

I don’t really see the purpose of binding the command seems like an extra step, when you could just do this.

commands = 
{
   ["command name"] = 
   {
       name = "kill",
       perm = 1,
       -- etc
   }
}

Next, I love the use of message:match("^"…prefix) in order to get the prefix, however this doesn’t support prefixes that are more than a length of 1, due to this line

local messageWithoutPrefix = message:sub(2,#message)

Next, I wouldn’t do this, keep your commands lowercase. As you made your kill command “kill” not “Kill”

local properCommand = command:sub(1,1):upper() .. command:sub(2,#message):lower()
-- This converts something like "fLy" into "Fly"

commands[command] -- Indexing the command won't work!

Other than that, I would look into potentially looking to allow multiple commands in one line, you can use string.split() for that, and split the string each time your prefix comes up. And I would look into adding comma support so that you can do ;kill me,myfriend,myenemy and that would work

2 Likes

I really don’t understand why you would do something more complicated and less secure here. It makes much more sense to use player.Chatted. Also, why are you using DataStore2, or even DataStore at all, here? With player.Chatted, none of this would happen. You just use

if player.UserId == 1 then
   -- commands here
end

and even with this module commands, just put a list in the settings of the command.

1 Like

properCommand is for error messages, a different variable is used for finding the command in the commands table.

I also updated the script so its possible to use prefixes with multiple characters, thanks for bringing this to my attention. :smiley:

1 Like

The data store is if you want to make a ban command or admin other players, otherwise you’d have to edit your code every time you wanted to admin/ban somebody.

1 Like