Text Filtering

One of the most important ways to keep games safe and secure is to apply proper text filtering. Roblox has a text filter feature that not only prevents users from seeing profane or inappropriate messages, but also blocks personally identifiable information. Roblox has many young users who should be safeguarded against sharing or seeing certain content.

Because filtering is so crucial for a safe environment, Roblox actively moderates the content of games to make sure they meet certain standards. If a game is reported or automatically detected to not use filtering, that game will be shut down until the developer takes the proper steps to apply filtering.

How to filter text[edit]

Text filtering is done with the FilterStringAsync function of TextService. This function will take a string of text as input and the userId of the player who created the text and return a TextFilterResult object that can be used to distribute the filtered string.

FilterStringAsync should be called on the server as it will fail if called on the client.

FilterStringAsync[edit]

FilterStringAsync can be used to filter a string intended for a single user to view or for broadcasting a string globally. It takes two arguments: the string to filter and the id of the player who authored the text. In code this function would appear as:

local TextService = game:GetService("TextService")
local filteredTextResult = TextService:FilterStringAsync(text, fromPlayerId)

This function can be used to filter strings meant for specific users or all users in chat and non-chat situations. TextFilterResult, the object returned by the function, has three methods that can be called:GetChatForUserAsync, GetNonChatStringForBroadcastAsync, GetNonChatStringForUserAsync.

Note that FilterStringAsync and all of the TextFilterResult functions can fail on occasion as they make web calls internally, so they should always be wrapped in pcalls.

local TextService = game:GetService("TextService")
local filteredText = ""
local success, errorMessage = pcall(function()
	filteredTextResult = TextService:FilterStringAsync(text, fromPlayerId)
end)
if not success then
	warn("Error filtering text:", text, ":", errorMessage)
	-- Put code here to handle filter failure
end
If the pcall that contains the filter function fails, it is important not to continue using the text as intended. It is better to have an empty text field than to have unfiltered text.

Example[edit]

This example sets up a widget that allows a player to send a message to another. Such a widget needs at least two scripts: a local script to handle input and displaying messages, and a script to filter the messages on the server. Because this example has a player sending a message to another specific player, the GetChatForUserAsync function should be used.

This tutorial assumes that a GUI and a remote event has been set up already.

-Text Filtering Example

-- LocalScript
 
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
 
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local screen = playerGui:WaitForChild("MessageScreen")
local sendMessageEvent = ReplicatedStorage:WaitForChild("SendPrivateMessage")
 
-- GUI elements for send frame
local sendFrame = screen:WaitForChild("SendFrame")
local recipientField = sendFrame:WaitForChild("Recipient")
local writeMessageField = sendFrame:WaitForChild("Message")
local sendButton = sendFrame:WaitForChild("Send")
 
-- GUI elements for receive frame
local receiveFrame = screen:WaitForChild("ReceiveFrame")
local senderField = receiveFrame:WaitForChild("From")
local readMessageField = receiveFrame:WaitForChild("Message")
 
-- Called when send button is clicked
local function onSendClicked()
	-- Try to find the recipient. Only want to send message if recipient exists
	local recipient = Players:FindFirstChild(recipientField.Text)
	local message = writeMessageField.Text
	if recipient and message ~= "" then
		-- Send the message
		sendMessageEvent:FireServer(recipient, message)
		-- Clean up send frame
		recipientField.Text = ""
		writeMessageField.Text = ""
	end	
end
 
-- Called when send message event fires meaning this client got a message
local function onReceiveMessage(sender, message)
	-- Populate fields of receive frame with the sender and message
	senderField.Text = sender.Name
	readMessageField.Text = message
end
 
-- Bind event functions
sendButton.MouseButton1Click:Connect(onSendClicked)
sendMessageEvent.OnClientEvent:Connect(onReceiveMessage)


-- Server Script
 
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")
 
local sendMessageEvent = ReplicatedStorage.SendPrivateMessage
 
local function getTextObject(message, fromPlayerId)
	local textObject
	local success, errorMessage = pcall(function()
		textObject = TextService:FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	end
	return false
end
 
local function getFilteredMessage(textObject, toPlayerId)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetChatForUserAsync(toPlayerId)
	end)
	if success then
		return filteredMessage
	end
	return false
end
 
-- Called when client sends a message
local function onSendMessage(sender, recipient, message)
	if message ~= "" then
		-- Filter the incoming message and send the filtered message
		local messageObject = getTextObject(message, sender.UserId)
 
		if messageObject then
			local filteredMessage = getFilteredMessage(messageObject, recipient.UserId)
			sendMessageEvent:FireClient(recipient, sender, message)
		end
	end
end
 
sendMessageEvent.OnServerEvent:Connect(onSendMessage)


Example[edit]

This example sets up a dialog that lets a player write a message on a sign. Since anyone in the server would be able to read the sign, even players who join the game after the writing player has left, the text has to be filtered with GetNonChatStringForBroadcastAsync. This example assumes that a place has been set up already with all the proper elements in place. A working place has been provided below.

Working Example -Broadcast Text Filtering

-- LocalScript
 
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
 
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local screen = playerGui:WaitForChild("MessageScreen")
 
-- GUI elements for dialog
local frame = screen:WaitForChild("Frame")
local messageInput = frame:WaitForChild("Message")
local sendButton = frame:WaitForChild("Send")
 
-- RemoteEvent to send text to server for filtering and display
local setSignText = ReplicatedStorage:WaitForChild("SetSignText")
 
-- Called when button is clicked
local function onClick()
	local message = messageInput.Text
	if message ~= "" then
		setSignText:FireServer(message)
		frame.Visible = false
	end
end
 
sendButton.MouseButton1Click:Connect(onClick)


-- Server Script
 
--local Players = game:GetService("Players")
local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
 
local sign = game.Workspace.Sign
local signTop = sign.Top
local signSurfaceGui = signTop.SurfaceGui
local signLabel = signSurfaceGui.SignLabel
 
local setSignText = ReplicatedStorage.SetSignText
 
local function getTextObject(message, fromPlayerId)
	local textObject
	local success, errorMessage = pcall(function()
		textObject = TextService:FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	elseif errorMessage then
		print("Error generating TextFilterResult:", errorMessage)
	end
	return false
end
 
local function getFilteredMessage(textObject)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetNonChatStringForBroadcastAsync()
	end)
	if success then
		return filteredMessage
	elseif errorMessage then
		print("Error filtering message:", errorMessage)
	end
	return false
end
 
-- Fired when client sends a request to write on the sign
local function onSetSignText(player, text)
	if text ~= "" then
		-- filter the incoming message and send the filtered message
		local messageObject = getTextObject(text, player.UserId)
		local filteredText = ""
		filteredText = getFilteredMessage(messageObject)
		signLabel.Text = filteredText
	end
end
 
setSignText.OnServerEvent:Connect(onSetSignText)

When to Filter Text[edit]

Any displayed text that a developer does not have explicit control over should be filtered. In general, this mainly refers to text that players have control over but there are a few other cases that are important to consider to make sure games are compliant with the Roblox filtering rules.

Player Input[edit]

Any text that a player writes that is to be displayed must be filtered, no matter how the text is input or displayed. The most common way to input text is through TextBoxes, but there can be any number of ways to get text input from a player, from a custom GUI with character buttons to interactive keyboard models in the 3d space.

AlternateInput0.png

AlternateInput1.png

Along with novel and unorthodox input methods, there are many ways of displaying text besides using TextLabels. For example, words can be spelled out with 3d parts, and Models with Humanoids display their names. If the content of any such display is visible to players, and if another player generated that content, then the text needs to be filtered before it is displayed.

AlternateDisplay0.png

AlternateDisplay1.png

Random Words[edit]

Some games may find it useful to generate words from random characters that are then displayed to players. There is a chance that such generations could create inappropriate words. In such situations the displayed result of random words should be sent through a filter on the server. In such cases the user id of the player who is going to be viewing the words can be used in FilterStringAsync.

For example, the following code sends a random word to players when they join the game (which will be displayed later). The code will generate random words in a loop until it finds one that has not been altered by the filter. This example assumes a bit of set up. A place file is included here: Working Example -Broadcast Text Filtering

The generator makes sure that the word that is sent has been filtered first:

local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
 
local sendRandomWordEvent = ReplicatedStorage.RandomWordEvent
local ALPHABET= "abcdefghijklmnopqrstuvwxyz"
local MIN_LENGTH = 3
local MAX_LENGTH = 8
-- Function to generate a random word
local function generateRandomWord()
	local length = math.random(MIN_LENGTH, MAX_LENGTH)
	local text = ""
	for index = 1, length do
		local randomLetterIndex = math.random(1, string.len(alphabet))
		text = text .. string.sub(ALPHABET, randomLetterIndex, randomLetterIndex)
	end
	return text
end
 
local function getTextObject(message, fromPlayerId)
	local textObject
	local success, errorMessage = pcall(function()
		textObject = TextService:FilterStringAsync(message, fromPlayerId)
	end)
	if success then
		return textObject
	elseif errorMessage then
		print("Error generating textObject")
	end
	return false
end
 
local function getFilteredMessage(textObject, toPlayerId)
	local filteredMessage
	local success, errorMessage = pcall(function()
		filteredMessage = textObject:GetNonChatStringForUserAsync(toPlayerId)
	end)
	if success then
		return filteredMessage
	elseif errorMessage then
		print("Error filtering message",errorMessage)
	end
	return false
end
 
-- Called when player joins the game
local function onPlayerJoined(player)
	local text = ""
	local filteredText = ""
	-- Generate random words until one is created that passes the filter
	repeat
		filteredText = ""
		text = generateRandomWord()
		-- filter the incoming message and send the filtered message
		local messageObject = getTextObject(text, player.UserId)
		filteredText = getFilteredMessage(messageObject, player.UserId)
	until text == filteredText
	if text == filteredText then
		print("The message is",text,"The filtered message is",filteredText)
	end
	-- Send the random word to the client
	sendRandomWordEvent:FireClient(player, text)
end
game.Players.PlayerAdded:Connect(onPlayerJoined)

External Sources[edit]

Some games connect to external web servers. In some cases this is used to fetch content that is used to display information in game. If the content of the external site is not in full control of the developer and it is possible for a 3rd party to edit the information, then that content should be filtered if it is to be displayed.

local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
 
local sendRandomName = ReplicatedStorage.SendRandomName
local randomNameWebsiteAddress = "http://www.roblox.com/randomname"
local nameTable = nil
 
local function initializeNameTable()
	local nameTableJSON = nil
	local success, message = pcall(function()
		nameTableJSON = HttpService:GetAsync(randomNameWebsiteAddress)
	end)
	if success then
		nameTable = HttpService:JSONDecode(nameTableJSON)
		print("The nameTable is:",nameTable)
	end
end
 
local function onPlayerJoin(player)
	if nameTable then
		local randomName = ""
		local filteredName = ""
		local filteredNameObject
		repeat
			randomName = nameTable[math.random(#nameTable)]
			local success, errorMessage = pcall(function()
			filteredNameObject = TextService:FilterStringAsync(randomName, player.UserId)
			end)
			if success then
				print("Success creating filtered object")
			elseif errorMessage then
				print("Error creating filtered object")
			end
			local success, errorMessage = pcall(function()
			filteredName = filteredNameObject.GetNonChatStringForUserAsync(player.UserId)
			end)
			if success then
				print("Success creating filtered name")
			elseif errorMessage then
				print("Error creating filtered name")
			end
		until randomName == filteredName
		sendRandomName:FireClient(sendRandomName)
	end
end
 
initializeNameTable()
game.Players.PlayerAdded:Connect(onPlayerJoin)

Stored Text[edit]

Many games will store text using DataStores. For example, games may store a chat log, or a player’s pet name, etc. In such cases, if the text that is being stored needs to be filtered, it is recommended to filter when retrieving the text. This ensures that the most up-to-date version of the filter is being used.

local TextService = game:GetService("TextService")
local DataStoreService = game:GetService("DataStoreService")
 
local petData = nil
local petCreator = require(game.ServerStorage.PetCreator)
 
local function onPlayerJoin(player)
	local data = {}
	local success, message = pcall(function()
		data = petData:GetAsync(player.UserId)
	end)
	if success then
		local petName = data.Name
		local petType = data.PetType
		local filteredName = ""
		local filteredNameObject
		local success, message = pcall(function()
			filteredNameObject = TextService:FilterStringAsync(petName, player.UserId)
		end)
		if success then
			local worked, errorMessage = pcall(function()
				filteredName = filteredNameObject:GetNonChatStringForBroadcastAsync()
			end)
			if worked then
				petCreator:MakePet(player, petType, filteredName)
			end
		end
	end
end
 
local success, message = pcall(function()
	petData = DataStoreService:GetDataStore("PetData")
end)
if success then
	game.Players.PlayerAdded:Connect(onPlayerJoin)
end

Exception[edit]

The one exception to text filtering is when it comes to displaying text to a player that they wrote themselves, although there are still some considerations to keep in mind.

Filtering text through the chat filter functions takes a bit of time. For example, suppose a player types a message that they want to display. That text has to be sent to the server, filtered, and then sent back to the client. Each of these stages takes a bit of time. When run in a sequence like this, there can be a noticeable delay between when a message is typed and the filtered message is returned.

FilterPath0.png

When sending a message to other players, this process is necessary (as the other players need to see the filtered text). But the player who wrote the message should see their own message in the log right away. With this in mind, there is a special edge case that Roblox has built in for the convenience of chat. If a player enters text using a TextBox specifically, the resulting text does not have to be filtered for that player and can be displayed to that player right away.

FilterPath1.png

An important caveat of this exception is when retrieving stored messages. The automated checks that Roblox does to detect if filtering is being done correctly knows to ignore text that was typed into TextBoxes, but only in the same session that the TextBox was used. If a player’s text is saved and then is retrieved later when the player rejoins the game, that saved text needs to be filtered before it is displayed to anyone, including the player who wrote it.