What are the arguments I should put for Chat:RegisterChatCallback()?

Hey everyone!

I’m trying to make a custom chat filter to fight back against these annoying chat bots that say, “oh, collect this in your browser, blah blah blah!”

I was wondering, what are the arguments for the Chat:RegisterChatCallback() function? I mean, what arguments should I put in my script?

Example:

function _C:Filter(player, message)  -- what do I put here?
	message = string.lower(message)
	
	for _, filteredWord in pairs(Filter) do
		if message:find(filteredWord) then
			message = "[flagged for inappropriate content.]"
		end
	end
	
	return message
end

Here’s my code where I use Chat:RegisterChatCallback():

local FilterService = require(game.ServerScriptService.CustomFilterService)
local ChatService = game:GetService("Chat")

local function relay()
	FilterService:Filter()
end

ChatService:RegisterChatCallback(Enum.ChatCallbackType.OnServerReceivingMessage, relay)

game.Players.PlayerAdded:Connect(function(player)
	player.Chatted:Connect(function(message)
		ChatService:InvokeChatCallback(Enum.ChatCallbackType.OnServerReceivingMessage, player, message)
	end)
end)

I just can’t find these anywhere I look.

Thanks! :slight_smile:

3 Likes

WARNING: This ended up being a very long post with lots of information. Read at your own peril.


While my typical response might have been read the documentation, unfortunately the documentation isn’t completely clear so you have to make assumptions based on the wording and I can’t assume that you’ll know how to interpret the wording either. Anyhow: RegisterChatCallback documentation.

I will explain from the top.

The way chat callbacks works are two-way: Roblox uses InvokeChatCallback, you use RegisterChatCallback. InvokeChatCallback is irrelevant to you (don’t call it, don’t think you can anyway), so focus on RegisterChatCallback. This will allow you to have a certain function ran when Roblox calls InvokeChatCallback.

For every use of RegisterChatCallback, you will always have the following two commonalities and differences at minimum:

Common:

  • Two parameters, the first is a ChatCallbackType Enum and the second is a function

  • The function you pass must return something

Different:

  • Roblox will call InvokeChatCallback with different arguments for different types of ChatCallbackTypes. You need to account for that. You can find out what by reading the documentation or reading through the LCS source code.

  • What you need to return will differ depending on the circumstances. Again, also explained in the documentation but no code samples are given so novice developers working with the LCS may have trouble here.

For OnServerReceivingMessage, Roblox will pass a ChatMessage object as a parameter. This will allow you to process the unfiltered message your own way first. In your function, you will only need the parameter messageObj (this is canonical if you want to adhere to LCS conventions). In code format, you’ll have a setup like this:

-- I'm applying my own conventions so you can see it easier
local SERVER_RECEIVE_MESSAGE = Enum.ChatCallbackType.OnServerReceivingMessage

local function serverMessageCallback(messageObj)
    -- Some code here
end

ChatService:RegisterChatCallback(SERVER_RECEIVE_MESSAGE, serverMessageCallback)

OnServerReceivingMessage chat callbacks will allow you to, from the server, make changes to the message object. Valid changes are available in the ChatMessage object documentation, such as changing what the message is or even its extra data (chat tags, chat colour, etc). There is one property that you can set as well which isn’t documented: ShouldDeliver. Setting it to false will prevent the server from sending the message. You can use this for things like chat blacklists.

All that clear? Great, so now let’s go into addressing your actual problem, which is just confusion on how to use this chat callback. I could just give you the solution, but if you don’t have that knowledge prior, things could stay confusing. It’s always good to pick up a thing or two when you resolve problems for future reference.

First off, get rid of the entire PlayerAdded connection, you won’t need that. Don’t ever use InvokeChatCallback by yourself, the Lua Chat System will do it for you.

Now, you should be left with two parts: the module’s code (your FilterService) and the callback registration code. We’ll address the callback registration code first because it’s easier and quicker to resolve, plus I gave an entire information TED talk about this callback.

Using the information gained from above, you’ll update your relay function to receive the messageObj as well as to return it back. Just a few simple changes. Your code should end up like this:

(Pre-code note: I use RobloxChatService for GetService-Chat so it doesn’t conflict with the ChatService module from the Lua Chat System.)

local FilterService = require(game:GetService("ServerScriptService").CustomFilterService)
local RobloxChatService = game:GetService("Chat")

local function relay(messageObj)
    -- We'll send the raw message over to your FilterService to maintain its
    -- versatility beyond just being for the Lua Chat System.
    local filteredMessage = FilterService:Filter(messageObj.Message)

    -- With the newly filtered message, we will update the message and
    -- have this given back to the LCS.
    messageObj.Message = filteredMessage

    return messageObj
end

ChatService:RegisterChatCallback(Enum.ChatCallbackType.OnServerReceivingMessage, relay)

Now, like I said earlier, while resolving this problem I want to retain the versatility of your FilterService so I don’t end up suggesting a change that’ll make it incompatible with any other uses for it you have (non-chat system filter requirements).

To explain what I did here: I sent the message over to FilterService:Filter to be processed as it deals with strings, which will then be given back to the relay function after processing. It will update the messageObj with the new filtered message and give all of this back to the chat system.

Now some changes for _C:Filter. First, remove the player parameter. It is possible to get a player from a messageObj but do remember that the Lua Chat System also allows NPC speakers, so a ChatSpeaker may not necessarily have a player.

The next thing you want to do is make a copy of the string rather than to operate on the message passed as an argument. Your current code will convert the given message into all lowercase letters to do the filter. That means that if your code doesn’t flag the message, what will be given back is a lowercase string, meaning anything ever sent in your game will be lowercase.

Remember, with Lua, changes aren’t made directly on a string, a new string is generated with the changes made. So we can put that new string into a variable and operate on that instead. From there, another boolean can accompany us to determine if the message was filtered or not. We can either return the flagged message default or the raw message if it wasn’t flagged.

function _C:Filter(message)
    -- Make changes to a proxy
    local messageCopy = message:lower()
    local wasFlagged = false

    -- If this isn't a dictionary, use ipairs instead
    for _, filteredWord in pairs(Filter) do
        -- Find filtered words in the proxy string: lower the current
        -- filter word in iteration just in case as well if you forget
        -- to make one lowercase.
        if messageCopy:find(filteredWord:lower()) then
            wasFlagged = true
            -- This loop doesn't need to run anymore the moment a true
            -- result is found, so prematurely terminate it.
            break
        end
    end

    -- You can use Lua ternary here, but for others to understand better,
    -- I will use if statements to better show what's up.
    if wasFlagged then
        -- Give back a string about how the message was flagged. If you review
        -- the code in relay, this effectively becomes the new message.
        return "[flagged for inappropriate content.]"
    else
        -- Give back the original message.
        return message
    end
end

That should be all to it. Try this stuff out, do some debugging if it doesn’t work or give me a nice green mark, the thing we all know and love.

Wait though. Can’t send you off just yet, because there’s still a chance for me to sneak some learning in here. Remember that chat bots send messages and that your filter will just replace the message out. The message you write can still be flagged by Roblox’s filter and you’ll still have spam in your chat bar. For UX’s sake, I prefer to outright decline sending the message.

Remember the undocumented property I talked about earlier, ShouldDeliver? We can use that to our advantage. By preventing the message from actually ever getting sent in the chat window, not only will you have blocked the scam message from appearing but players won’t have their chat clogged up with “flagged” which would be equally as annoying as seeing the scam message in the first place. Bonus round: Chatted still fires, meaning accounts can be moderated if reported.

If you prefer to take this route (and I strongly encourage you do!), you’ll have to change up your filter a bit or make a new function. It can be non-returning, we’ll just be calling this to determine if we should add in the ShouldDeliver property to the message object. I’ll write from the perspective of fundamentally rewriting Filter but you have the option to make this a separate function.

So, we can still reuse the Filter function written earlier, except this time we’ll return wasFlagged regardless of what happens to it and we’ll also change when it becomes true/false. Remember, ShouldDeliver = false means it won’t send. _C:Filter will now look like this:

(Pre-code note: would advise a new named function so it’s clear what the intention of the function is. For example, CheckShouldDeliver, so that you can get the nice line of local shouldDeliver = FilterService:CheckShouldFilter(messageObj.Message).)

function _C:Filter(message)
    local messageCopy = message:lower()
    -- We make this shouldDeliver now
    local shouldDeliver = true

    for _, filteredWord in pairs(Filter) do
        if messageCopy:find(filteredWord:lower()) then
            -- Flagged word will make this false instead
            shouldDeliver = false
            break
        end
    end

    return shouldDeliver
end

Now back to relay. What we’re going to do is run the message from messageObj through this newly (re)written function to get back a boolean which will determine if we should put a ShouldDeliver property in the messageObj. We’ll then give that back to the LCS which will, if it finds ShouldDeliver to be false, drop the message from the send queue. Relay will become this:

local function relay(messageObj)
    -- We'll continue to send the message, but this time what we want
    -- back is not a string but a boolean.
    local shouldDeliver = FilterService:Filter(messageObj.Message)

    -- Remember: in Lua, keys still exist in tables but are nil. The way LCS
    -- checks is explicitly if ShouldDeliver is false. So, keeping in mind
    -- both how Lua can read table indices and how LCS processes
    -- the ShouldDeliver key, we can indiscriminately set the key.
    messageObj.ShouldDeliver = shouldDeliver

    -- Once again, give the messageObj back, this time though we only allow
    -- the LCS to continue processing and sending the message *if* we
    -- don't flag it from _C:Filter.
    return messageObj
end

And like that, you’ve also got a better player experience on top of blacklisting scam messages. Cool, right? I’ve only worked with OnClientFormattingMessage and OnCreatingChatWindow before, never OnServerReceivingMessage, so I learned a couple of things while typing this response up.

It’s my first time working with OSRM and in a support topic no less, so mistakes could be possible. Please do not hesitate to alert me if you run into a problem and I’ll try to help you to resolve it.

The Lua Chat System has great documentation but it lacks in some areas. That’s why I think of Developer Hub content requests to write about or in other cases, write Development Resources on it. I like to think that I’ve grabbed hold of a sufficient understanding of the LCS altogether. As a little plug, albeit off-topic, an example is sharing how you can change chat settings through RegisterChatCallback without forking anything.

A long response for a simple problem. Sorry for the long reading! I’m not too great with concise articulation and I also had a lot of material to go over for this. Hope you’ve learned something new along with being able to resolve your problem.

Cheers! :slight_smile:

2 Likes

Hello! Sorry for the long response, I’m just reading through this.

I was experiencing something, though. It prints the messageObj.Message string as the filtered one when filtered, and not when not filtered, but the thing is, the actual chat message doesn’t work. Is there anything you can recommend me something to do?

I want to continue using default roblox chat, but I can make my own chat.

EDIT: This is my code in the OnChatted script:

local ChatService = game:GetService("Chat")

local function relay(messageObj)
	print("relay function called")
	local filteredMessage = FilterService:Filter(messageObj.Message)
	
	messageObj.Message = filteredMessage
	
	print(filteredMessage .. " vs " .. messageObj.Message)
	return messageObj
end

ChatService:RegisterChatCallback(Enum.ChatCallbackType.OnServerReceivingMessage, relay)

and then my code in the FilterService script:

local _C = {}
local Filter = require(script.Filter)
local Settings = require(script.Settings)

function _C:Filter(message)
	print(message)
	local messageCopy = message:lower()
	local wasFlagged = false
	
	for _, filteredWord in pairs(Filter) do
        if messageCopy:find(filteredWord:lower()) then
            wasFlagged = true
            break
        end
    end
	
	if wasFlagged == true then
		return "flagged."
	else
		return message
	end
end

return _C
1 Like

Oh, yeah, I can see that myself actually and I understand where you’re coming from. The message filter works exactly as expected, except in the chat window it doesn’t show up as the flagged message.

Just as a little test, I wanted to check and confirm the behaviour of OnServerReceivingMessage so I added a debug line that would recolour the chat red if the message was flagged. Using the code you supplied, along with replacements for unsupplied bits (e.g. submodules), I added this before setting messageObj.Message to the filtered string:

if filteredMessage ~= messageObj.Message then
	messageObj.ExtraData.ChatColor = Color3.new(1, 0, 0)
end

image

So judging from this test, it seems that OnServerReceivingMessage allows you to process the message based on the content sent but it internally caches the original message. I find that to be a little strange. I think if I interpret this a different way, you can change how the message is processed but not the message itself. That would make things unfortunate… especially since I spent so much time writing around a potential misunderstanding…

Not to worry, this situation can still be salvaged. There’s a reason why I had a backup, which was the bottom half of the post. I find that this would be the better way to do it: instead of replacing your message with empty content, you can prevent the message from being sent at all. Try it out and see how you like it. I ran a quick test in Studio, worked as expected.

If you want a lazy low-level solution to block the message with your current code, just change relay as follows:

local function relay(messageObj)
	print("relay function called")
	local filteredMessage = FilterService:Filter(messageObj.Message)
	
	if filteredMessage ~= messageObj.Message then
        messageObj.ShouldDeliver = false
    end
	
	print(filteredMessage .. " vs " .. messageObj.Message)
	return messageObj
end

If you’re still interested in having the message sent but changing it to a message like flagged, I can look into that and provide you some guidance and samples for doing that. Let me know if you’d like that or if you want to opt to just block the message altogether.

Thanks for raising this issue to me. Again, it was my first time working with this and I may have misinterpreted how this is used, so the mistake was bound to happen. Just kind of disheartened that I was so happy doing that explanation and it turns out I misunderstood. Sorry! :frowning:

3 Likes

Alrighty!

Don’t worry about it. I feel you, I asked the question because I didn’t know. Haha.

Up to you. I’d prefer to have the message sent, so that players can see (I just think it looks good to show that I care about this issue, and also to make sure it’s working properly when I play), but if you don’t want to (or don’t have the time, etc.) I can make due with print lines, custom Developer Consoles, etc.

Thanks for your time! :slight_smile:

1 Like

Gotcha. The most immediate example I can think of is by using the Lua Chat Service’s native extensions capabilities. You won’t require any forks, but you will have to pre-create some bits of it. Luckily it also has options to fill in for defaults.

For this, you can refer to Lua Chat System → Server Modules → Filter Functions. This, on the other hand, will allow you to directly work on the message - it’s a filter function after all. That would mean that you can transfer the entire system to a module that’ll be ran by the system instead of independently. That being said, it’s not like you can’t do it independently either: the principles are the same.

Now between independently and having it ran by the ChatService are not all that different. If you have the system functioning independently, you will just have to access the required pieces differently. If the Lua Chat System runs it, you’ll need some boilerplate code but everything stays relatively the same.

Pick the method you like. Since I’m working to explain from two different perspectives, I will be focusing mainly on the things common between each method and not the specifics. This is so that you can approach the system differently but still have the same outcome.

Steps for setting up boilerplate as a Lua Chat System extension

Create a Folder named “ChatModules” in the Chat service. Also create a BoolValue named “InsertDefaultModules” and put it into this folder. Make sure the value is checked off. This will allow us to add our own modules to the Lua Chat System without actually forking any components and having to maintain them manually.

Next, create a new ModuleScript, any name you like (like CustomFilter, ExtraFilter or ScamFilter, whatever you like). This will go into that ChatModules folder. As I’m going along typing this and testing it at the end, I will call mine ScamFilter.

The boilerplate for a filter function is provided on the documentation page: replace the default ModuleScript contents with this. So that it’s visible here on this post and you don’t have to go hunting it down yourself or switching tabs to follow along, here:

local function Run(ChatService)
	local function filterFunction(sender, messageObject, channelName)
		
	end
	
	ChatService:RegisterFilterMessageFunction("FILTER_FUNCTION_NAME", filterFunction)
end
 
return Run
Steps for setting up code as an independent system

If you prefer to just have your own script do everything, that’s fine too! Canonically your filter function should be an extension as shown above but filter functions are only ran when a message is processed up to the point of having to run filters, so it’s fine to do later.

The only real difference here is that you won’t have access to the ChatService. A ChatModule will have the ChatService passed so it has easy access for it: you need to fish it out. Doesn’t take much for that to happen and the code still looks the same.

local ServerScriptService = game:GetService("ServerScriptService")
local ChatService = require(ServerScriptService:WaitForChild("ChatServiceRunner").ChatService)

local function filterFunction(sender, messageObject, channelName)

end

ChatService:RegisterFilterMessageFunction("FILTER_FUNCTION_NAME", filterFunction)

Notice how both of these are pretty much the same? The Lua Chat System extension runs as a module and gives you ChatService automatically, while an independent system just needs to access ChatService. Once you have that ChatService, the rest is exactly the same.

Now this is a little different from the previous spiel I gave. Unlike RegisterChatCallback, we don’t have to return anything. We can operate directly on the message, including change how it’s processed (making it more powerful)! The workload will be relatively the same as before.

So like before, you can keep your FilterService independent. Since you said you prefer to still have the message sent rather than to block it outright, we’ll keep going with that. Your FilterService function can then remain the same as the original where we take the message and decide whether to show the original message or the flagged one.

To refresh you and others who may be following along, here is the _C:Filter function. The full ModuleScript code is available above for a reference.
function _C:Filter(message)
	local messageCopy = message:lower()
	local wasFlagged = false
	
	for _, filteredWord in pairs(Filter) do
        if messageCopy:find(filteredWord:lower()) then
            wasFlagged = true
            break
        end
    end
	
	if wasFlagged == true then
		return "flagged."
	else
		return message
	end
end

So once again, from this new filter function, the difference will be that we get to modify the message directly. The messageObject is still there and everything. To simplify a long story short: you’re writing the same code from relay in your filterFunction, except you won’t return anything.

-- filterFunction is what relay was: we're just changing messageObj to
-- messageObject to follow conventions with the boilerplate.
local function filterFunction(sender, messageObject, channelName)
    local filteredMessage = FilterService:Filter(messageObject.Message)

    -- We can indiscriminately set because of how Filter returns
    messageObject.Message = filteredMessage
end

Woah! Done! That’s it? Yeah that is. Salvaged once again! But I’m still going to write a resource on OnServerReceivingMessage anyway, now that I have new knowledge…

If you need a reference, here are finished code samples using each approach to the filter functions.

Finished code sample: Lua Chat System extension
local FilterService = require(game:GetService("ServerScriptService").CustomFilterService)

local function Run(ChatService)
	local function filterFunction(sender, messageObject, channelName)
	    local filteredMessage = FilterService:Filter(messageObject.Message)
	
	    -- We can indiscriminately set because of how Filter returns
	    messageObject.Message = filteredMessage
	end
	
	ChatService:RegisterFilterMessageFunction("ScamFilterFunction", filterFunction)
end
 
return Run
Finished code sample: Independent system
local ServerScriptService = game:GetService("ServerScriptService")
local ChatService = require(ServerScriptService:WaitForChild("ChatServiceRunner").ChatService)
local FilterService = require(ServerScriptService.CustomFilterService)

local function filterFunction(sender, messageObject, channelName)
	local filteredMessage = FilterService:Filter(messageObject.Message)
	
	-- We can indiscriminately set because of how Filter returns
	messageObject.Message = filteredMessage
end

ChatService:RegisterFilterMessageFunction("ScamFilterFunction", filterFunction)

* Little note: I experienced some weird behaviour with it as a LCS extension. My name colour wasn’t preserved when done from that, but it was when I had it independent.

Here’s a repro file if you aren’t able to follow along well or if you’re more of a hands-on person.

CustomFilterService Repro.rbxl (19.7 KB)

HOW TO USE:

  1. Select a method of your choice from ServerStorage, don’t use both though. ScamFilterScript can be moved to ServerScriptService or ChatModules to Chat.

  2. Run a test server (F5). When your player loads, start typing. If at any point you include the word “test” in your chat (or anything you add in the table under ServerScriptService.CustomFilterService.Filter), it will show up as “flagged.”. Examples you can type out are “test”, “…test”, “test!”, “foo test bar”, so on.

  3. Profit.

Hopefully this leaves you all set and ready to go. Let me know how this goes! I think it might be a better solution altogether since all the conventions and method names actually scream “filter” instead of “OnServerReceivingMessage” which sounds - and I found out, behaves - as just a way to change how the message is processed or if it’s allowed to get sent, haha.

:slight_smile:

1 Like

Hey all–Just to add a probably easier solution for the bot problem…
A chat module is also used for grabbing admin commands–Even though it’s not named as a “filtering”
function, using ChatService:RegisterProcessCommandsFunction() my be better suited for bots, ie.
Just return true from your registered bot-check function when the bot/text is detected, and no one
else in the chat will see or know that anything happened.

Thanks so much! It means a lot! Holy man, you took a lot of time and that and I greatly appreciate it. :slight_smile:

I’m using the Independant version. I like having my own code, because then I know that I went that extra step to make it my own. I like making custom services the most.

Thanks again!

EDIT: I’ll mark it as a solution when I’m done changing my scripts, to make sure

1 Like

Yeah. I just wanted to be able to see.

Thanks for your effort to help!

Works! Thanks so much! :smiley:

1 Like