PcoWSkbVqDnWTu_dm2ix
We use cookies on this site to enhance your user experience

Text and Chat Filtering

Text and Chat Filtering

Jul 03 2018, 9:35 AM PST 20 min

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

Text filtering is done with the TextService/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

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:TextFilterResult/GetChatForUserAsync, TextFilterResult/GetNonChatStringForBroadcastAsync, TextFilterResult/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

Example

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.

-Text Filtering Example

LocalScript

-- 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

-- 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

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 TextFilterResult/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

-- 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

-- 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

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

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 beside 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

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

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

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

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.

Tags:
  • filtering
  • security