Cleaner chat: profanity and link filtering
Both the lobby and in-game chat now block profanity from a 40,000-term list and reject links before a message is ever stored.

In plain words
Chat should be welcoming and safe, in any language. Every message you send — in the lobby and inside a game — is now checked before it is posted. Profanity is blocked, and links are not allowed, so the chat stays about the game.
The checks are careful, not clumsy: they match whole words and phrases, so an innocent word that merely contains a blocked substring still gets through. Stickers are a fixed, safe set, so they are never affected.
For the technically curious
A new @opolyx/moderation package (modeled on the logger package) does exact whole-word and phrase matching, per the blocklist’s own usage notes: “pain in the ass” blocks, but “assassin” and “horde” pass — there is no substring scan, and the list already pre-enumerates obfuscations like h0rny, so no fuzzy normalization is needed. It lowercases, tokenizes on whitespace, tries raw and edge-punctuation-trimmed variants against a Set, then checks phrase n-grams of 2 to 11 words.
The 40,917 terms live in a generated string-literal module (~420 KB) that is server-side only — the client chunks were verified clean. Both chat write paths reject a bad message before the insert, because Realtime fans stored rows out verbatim, so the check has to precede the write.
// link filter: schemed URLs (https?|ftp|wss?://, hxxp), www., and bare
// label.tld against a curated ~65-TLD set — so Node.js / e.g. / "gg wp" pass,
// but discord.gg / t.me/x / mysite.com / emails are blocked. Light dot-de-obfuscation
// folds example[.]com and "site dot net" first.One accepted gap (owner decision): a direct database insert bypasses these action-level filters — the hardening candidate is a banned-terms table plus a before-insert trigger. Nickname filtering and a fully localized error string are deferred.
