Chat Guard v2.0 | A captchaless, easy to implement anti-chat bot system

Notice

This system is designed for the lua chat system, which is in the process of being phased out. I don’t have any plans to maintain this anymore!


As you may know, the spam & scam botting problem on Roblox has been a rising issue. Its been affecting some games more than others and acts as a constant annoyance for your game’s users, as well as putting players who visit links sent by bots at risk. As a solution, I present Chat Guard.

Chat Guard is a low friction, easy to implement system for the built-in Roblox Lua Chat system. Its purpose is to try and protect your game from spam/scam bots by giving you a list of options to fine-tune. I’ll go over the details in this thread.


What does it do?

  • Provides protection without needing to fork or modify the Chat system.

  • Protects the SayMessageRequest remote from automation. This is a key part of the system, as bots rely on firing the remote to simulate a player sending a message to the server.

  • Provides a module with functions for further custom security measures. This allows you to add more measures, such as requiring players to click a button, walk somewhere on the map, wait for their loading screen to finish, etc. before they can send messages.

  • Shadow bans detected bots instead of kicking them. Their messages are only visible to themselves!

What it looks like when a user is blocked from chatting:


The user on the left is shadowbanned, whereas the user on the right is not.


How do you set it up?

Main script:

-- metavirtual, 2021
-- Responsible for protecting the chat remote on the server.

-- https://github.com/metaVirtual/ChatGuard

--[[
	*these functions are completely optional and aren't required for the bot protection that ChatGuard provides.*
	
	ChatGuard:TrustPlayer(player, trusted [default: true]) [yields]
	- Allows the selected player to send messages.
	- Useful if you want to require players to do certain actions before they can chat,
	- such as click a button or walk somewhere.

	ChatGuard:IsPlayerTrusted(player) [yields]
	- Check if a player is trusted. You may need set TRUST_PLAYERS_BY_DEFAULT to false, depending on your use case.
	- Returns a boolean.
-]]

-- If this is set to false, then players will not be able to talk until ChatGuard:TrustPlayer is called.
local TRUST_PLAYERS_BY_DEFAULT = true

local Players = game:GetService("Players")
local Chat = game:GetService("Chat")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Modules = script:WaitForChild("Modules")

local Signal = require(Modules.Signal)
local ChatService = require(game:GetService("ServerScriptService"):WaitForChild("ChatServiceRunner").ChatService)

local MESSAGE_CACHE_SIZE = 16

local ChatGuard = {ChatProfiles = {}, _running = false, _started = Signal.new()}

-- Simulate a message sending: game.ReplicatedStorage.DefaultChatSystemChatEvents.SayMessageRequest:FireServer("test", "All")

function ChatGuard._onPlayerAdded(player)
	ChatGuard.ChatProfiles[player] = {
		messages = {};
		shadowBanned = false;
		untrusted = (not TRUST_PLAYERS_BY_DEFAULT);
		chatBind = Instance.new("BindableEvent");
		messageValidated = false
	}

	local function OnChatted(message)
		ChatGuard.ChatProfiles[player].messageValidated = true

		table.insert(ChatGuard.ChatProfiles[player].messages, message)

		if #ChatGuard.ChatProfiles[player].messages > MESSAGE_CACHE_SIZE then
			table.remove(ChatGuard.ChatProfiles[player].messages, 1)
		end
	end

	player.Chatted:Connect(OnChatted)
end

function ChatGuard._onSayMessageRequest(player, message, channel)
	if ChatGuard.ChatProfiles[player].shadowBanned then
		return
	end

	local messageFound = false

	for i = 1, 3 do
		for i, data in pairs (ChatGuard.ChatProfiles[player].messages) do
			local messageString = data

			if messageString == message then
				messageFound = true

				-- Remove message from cache
				ChatGuard.ChatProfiles[player].messages[i] = nil

				break
			end
		end

		if messageFound then
			break
		end

		RunService.Heartbeat:Wait()
	end

	if not messageFound then
		print("Chat shadow banned", player)

		ChatGuard.ChatProfiles[player].shadowBanned = true
		return
	end
end

function ChatGuard._onPlayerRemoving(player)
	-- Clear player from memory
	ChatGuard.ChatProfiles[player] = nil
end

function ChatGuard._messageSwallower(SpeakerName, Message, ChannelName)
	local ChatLocalization = nil
	pcall(function() ChatLocalization = require(game:GetService("Chat").ClientChatModules.ChatLocalization) end)
	if ChatLocalization == nil then ChatLocalization = {} end

	if not ChatLocalization.FormatMessageToSend or not ChatLocalization.LocalizeFormattedMessage then
		function ChatLocalization:FormatMessageToSend(key,default) return default end
	end

	local speaker = ChatService:GetSpeaker(SpeakerName)
	local channel = ChatService:GetChannel(ChannelName)
	local player = speaker:GetPlayer()

	if player and speaker and channel then
		if ChatGuard.ChatProfiles[player].shadowBanned or not (ChatGuard.ChatProfiles[player].messageValidated) then
			-- Send the message to themselves
			speaker:SendMessage(Message, ChannelName, SpeakerName, Message.ExtraData)

			-- Swallow the message; this means that no other players will see it.
			return true
		elseif ChatGuard.ChatProfiles[player].untrusted then
			-- Tell the user that they have to 'wait before speaking'
			local timeDiff = 30

			local msg = ChatLocalization:FormatMessageToSend("GameChat_ChatFloodDetector_MessageDisplaySeconds",
				string.format("You must wait %d %s before sending another message!", timeDiff, (timeDiff > 1) and "seconds" or "second"),
				"RBX_NUMBER",
				tostring(timeDiff)
			)

			speaker:SendSystemMessage(msg, ChannelName)

			-- Swallow the message
			return true
		else
			if ChatGuard.ChatProfiles[player].messageValidated then
				ChatGuard.ChatProfiles[player].messageValidated = false

				-- Send the message
				return false
			end
		end
	end

	-- Send the message
	return false
end

function ChatGuard:Start()
	Players.PlayerAdded:Connect(self._onPlayerAdded)
	Players.PlayerRemoving:Connect(self._onPlayerRemoving)

	table.foreach(Players:GetPlayers(), function(_, player)
		self._onPlayerAdded(player)
	end)

	ChatService:RegisterProcessCommandsFunction("chat_guard_swallow", self._messageSwallower)
	ReplicatedStorage:WaitForChild("DefaultChatSystemChatEvents"):WaitForChild("SayMessageRequest").OnServerEvent:Connect(self._onSayMessageRequest)
	
	self._running = true
end

function ChatGuard:_waitForStart()
	if not self._running then
		self._started:Wait()
	end
end

function ChatGuard:TrustPlayer(player, trusted)
	self:_waitForStart()
	
	local untrusted = false
	
	if trusted ~= nil then
		untrusted = (not trusted)
	end
	
	if self.ChatProfiles[player] then
		self.ChatProfiles[player].untrusted = untrusted
	end
end

function ChatGuard:IsPlayerTrusted(player)
	self:_waitForStart()
	
	return self.ChatProfiles[player] and (not self.ChatProfiles[player].untrusted)
end

return ChatGuard

Conclusion

Thank your interest! Please let me know about any feedback you have or any issues you run into using the system in this thread. Feel free to post about your implementations of this system if you do decide to use it, I'd love to hear about it!
38 Likes

Very nice, I like the checks they are very smart.

Good resource, I also love how you make sure to include multiple verification checks including if the user has a verified email, their account age, and if they are in groups or have any friends.

I rate this resource a 10/10 and recommend anyone in need of scambot protection to use this.

2 Likes

I like the idea, but there is one thing I’m curious about -

Wouldn’t it be better when someone is shadowbanned to remove their chat message from other players aswell? Maybe even delete players gradually (locally), to simulate people leaving? Just a thought

1 Like

A little confused about what you mean, could you elaborate? Messages sent by shadowbanned users are never displayed to other players and only to themselves. :slightly_smiling_face:

1 Like

I somehow mistook the right side for being shadowbanned, nevermind

1 Like

Seems like a good start to preventing spam messages.

What is your plan if the people who run these bots makes the bot move, or bypass your checking procedure?

1 Like

(note, this answer is not valid for v2.0)

The system needs to be bypassed on a game-by-game basis. So in order for the current bots that target large amounts of games to remain effective with this system in place (or other anti-botting systems/measures), there would have to be enough unprotected games.

:tada: v2.0 has been released!

Rewrote the entire system!

  • Chat Guard is now fully server sided. No more installer required!
  • The system is now available through GitHub!
  • Introduced a brand new module that allows developers to implement their own chat security measures if wanted.
1 Like