PSA: You should refilter player's text even if it has gotten through the filter before

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.

16 Likes

Thanks for sharing this, will keep this in mind when developing and filtering out for text with malicious intents.

3 Likes

Isn’t it more or less a widely known fact that you have to filter text when loading something that contains user input? Imo this is blowing it out of proportion.

2 Likes

On the mention of substrings: this is typically why, in past experience, Roblox encourages that the filter API is used as the final layer and not any time before. You’ll notice that in the Legacy Chat System you can apply your own filters to chat (whether to change the message or to apply some effects) but after your developer-set filters run, the message passes through to TextService.

Roblox has also encouraged and welcomed developers to add their own layers of filtering if desired for content strings in their experiences but strings should be passed through TextService at the end so that the modified message goes through the appropriate filtering.

Filtering loaded strings that have to be displayed to any player including the one who made the string makes sure that strings use the most up-to-date context-aware filters. It’s also easier for you as a developer to not store filtered text because there are a lot of cases where you need to work with raw input such as chat-based commands.

Great pointer though, since I still see this question float around sometimes.

1 Like