You all know that it is very important to pass all user generated text through TextService:FilterStringAsync
before displaying it to the world.
For example, your game may have a character nametag which players can use any need to fill out. And considering if your game saves, you would also use a data store to store that nametag text. When you rejoin, the nametag will automatically be filled out to save the text.
Now, this 100% does not mean that you are safe from the hands of Roblox moderation potentially striking down your game one day for failing to bypass the filter. Which is strange, because your game does apply the filter, but what does it mean when I say that?
This is because societal context matters.
Something that may not be offensive a year ago may be offensive now. For example, take the Russian invasion of Ukraine. Mentioning several Russian terms on Roblox two years ago were not yet moderated because the conflict between the two countries have not been escalated into a serious world problem. But now, they are moderated.
And well, if you have an inappropriate term in today’s context that was appropriate before, stored in people’s Datastores to be automatically displayed without any check, you can get moderated.
Roblox even mentions this! Text Filtering | Documentation - Roblox Creator Hub
Stored Text - If your experience stores text such as a chat log or a user’s pet name using data stores, and the stored text might include inappropriate words, you should filter the text strings when retrieving them. This ensures that you use the most up-to-date version of the text filter.
Now how do you fix this?
It’s really easy. All you just have to do, is call FilterStringAsync
upon player join!
However, it can get pretty expensive to call such queries when your data file is full of text, such as having a house full of signs everywhere.
So instead have a “staggered” re-apply, meaning that when the user passes text to the server to store, store a Unix timestamp to schedule the reapplication. E.g. Such text will be scheduled to be re-applied 24 hours after being set initially.
someRemoteEventThatAllowsUserText.OnServerEvent:Connect(function(player, key, text)
-- Assuming you are using ProfileService
local Data = Profiles[player].Data
-- which is a dictionary that looks like this:
--[[
{
[Key] = {Text, UnixTime}
...
}
]]
local success, result = pcall(function()
return TextService:FilterStringAsync(text, player.UserId, Enum.TextFilterContext.PublicChat)
end)
if success then
Data[key] = {result, os.time() + 24*60*60} -- 24 hours
end
end)
game.Players.PlayerAdded:Connect(function(player)
-- load Profile
-- ...
local Data = Profile.Data
for key, text in pairs(Data) do
if text[2] > os.time() then
-- reapply the filter
end
end
end)
ALSO VERY IMPORTANT TO MENTION! You can also get moderated for truncated substrings in your game.
I made a post in #development-discussion a while ago but I assume that not all of you are DevForum regulars, so I’ll just put it here.
Let’s say the display on the sign of an in game house truncates whatever is on the name tag over 20 characters to 20. So “notonlytwentycharacters” would be “notonlytwentycharact”.
Let’s say that a theoretical display of truncation to 10 characters truncates the name tag of “eat my assassin’s poison cake”. Yeah, that’s a problem, you can see.
It’s a very harmless sentence. So therefore it is passed through the filter harmlessly. But the evil text appears on the sign of an in-game house- “eat my a##”.
So if you plan to display truncated substrings, make sure to filter that substring as well.