TextFilter
Filters text one player writes before other players see it, through TextService on the server, and returns nothing when filtering fails.
- Realm
- Server
- Require
ServerScriptService.ChloeKernelServer .TextFilter - Since
- v0.9.0
- Source
- TextFilter.luau
TextFilter runs player-written text through TextService:FilterStringAsync on the server before anyone else sees it. It
has two entry points: broadcast for text every player may see, and forUser for text one recipient sees. Both fail
closed: any error returns nil, and the caller then shows nothing. The raw text is never a fallback.
Player Settings rules take Filter = "Broadcast" to run the same filter on stored strings.
Text is a separate module that localizes the game’s own strings.
When a game must filter
Section titled “When a game must filter”Roblox requires text that one player writes and another player sees to pass the text filter, or the experience risks moderation. That covers pet names, nicknames, sign and plot text, guild and team names, item inscriptions, trade notes and any custom chat outside TextChatService. Filter on the server, where the client cannot skip it.
It does not cover the game’s own strings (item names, quest text) or chat sent through TextChatService, which the engine
filters. A ban’s DisplayReason is filtered by the engine as well (see Consequences).
How it works
Section titled “How it works”Broadcast
Section titled “Broadcast”filter:broadcast(session, text) calls FilterStringAsync with the author’s session.Player.UserId and
Enum.TextFilterContext.PublicChat, then result:GetNonChatStringForBroadcastAsync(). The result is filtered for the
strictest audience, so it is safe for every player in the server and for players who join later. It is the right call
for text that outlives the moment it was written: a sign, a guild name, a plot title.
For one recipient
Section titled “For one recipient”filter:forUser(session, text, toPlayer) calls FilterStringAsync with Enum.TextFilterContext.PrivateChat, then
result:GetNonChatStringForUserAsync(toPlayer.UserId). The result is filtered for that recipient’s age and settings. Use
it for text addressed to one player, such as a note attached to a trade offer.
The engine reference says the context argument does not change the result today; the wrapper passes the matching one anyway.
Failing closed
Section titled “Failing closed”Both functions yield and return a string or nil. They return nil when:
textis not a string, the session is not a table, or the session has noPlayer.forUsergets notoPlayer.FilterStringAsyncraises. It raises when the author is not in this server, among other failures.- The result method raises or returns something other than a string.
Neither function retries. FilterStringAsync retries internally, and the engine reference says not to retry it. Neither
caches, so each call is one filter request.
Settings rules with Filter = “Broadcast”
Section titled “Settings rules with Filter = “Broadcast””A Player Settings rule of kind "string" or "strings" can take Filter = "Broadcast".
Settings.attach raises for any other Filter value and for a non-string kind. After the validation chain passes, each
string of the value goes through broadcast, and the filtered result is what gets stored, synced and published. No path
can show the raw text, and the author sees the filtered version too.
A failed filter rejects the write. An acknowledged write answers { Accepted = false, Value } and the client reports
"Rejected" once, an intent write pushes a Settings_Sync rollback, and a server set returns false. A newer write to
the same key while one is still filtering supersedes it, and so does the session ending or a trade taking the profile
during the filter. A server set on a filtered rule yields while it filters.
A sign that shows text its owner wrote. The channel caps the length before the filter ever runs:
SetSignText = { Kind = "Intent", Schema = { "String" }, RateLimit = 1, Validate = { [1] = { MaxLength = 100 } },},--!strict-- SignService: each player's plot sign shows text they wrote, filtered for everyone.
local ServerScriptService = game:GetService("ServerScriptService")
local TextFilter = require(ServerScriptService.ChloeKernelServer.TextFilter)
local SignService = { Name = "SignService",}
function SignService.init(self: any, kernel: any) self.Kernel = kernel self.Filter = TextFilter.new() -- Latest submission per session; a slower filter call must not overwrite a newer one self.Tokens = setmetatable({}, { __mode = "k" })end
function SignService.start(self: any) self.Net.SetSignText.on(function(session, text: string) local Token = (self.Tokens[session] or 0) + 1 self.Tokens[session] = Token -- Yields. Filter at submission: the author must still be in the server. local Shown = self.Filter:broadcast(session, text) if Shown == nil or session.Active == false or self.Tokens[session] ~= Token then return -- failed, left or superseded: show nothing, never the raw text end local Sign = workspace:FindFirstChild(`Sign_{session.Player.UserId}`) if Sign then Sign:SetAttribute("Text", Shown) end end)end
return SignServiceStore the filtered result and drop the raw text. Text that outlives its author cannot be filtered later, because
FilterStringAsync raises once the author has left the server.
A note for one recipient uses forUser:
local Note = self.Filter:forUser(session, text, Recipient)if Note and Recipient.Parent then self.Net.TradeNote.send(Recipient, session.Player.UserId, Note)endPreferences that other players see are simpler through Settings, which filters, stores and syncs in one place:
local Prefs = Settings.attach(kernel, { Schema = { PetName = { Kind = "string", Default = "", MaxLength = 32, Filter = "Broadcast" }, MusicVolume = { Kind = "number", Default = 0.5, Min = 0, Max = 1 }, },})| Member | Description |
|---|---|
TextFilter.new(options?) |
Returns a filter. options.Service replaces TextService, for specs. Attaches nothing to the kernel. |
filter:broadcast(session, text) |
Yields. Text filtered for every player (PublicChat, GetNonChatStringForBroadcastAsync), or nil on any failure. |
filter:forUser(session, text, toPlayer) |
Yields. Text filtered for one recipient (PrivateChat, GetNonChatStringForUserAsync), or nil on any failure. |
Hooks & bus topics
Section titled “Hooks & bus topics”TextFilter fires no hooks and publishes nothing. Settings publishes Settings.Changed(session, key, value) with the
filtered value after a Filter = "Broadcast" write lands.