OOS Command not working on reset of the non OOS'd player

So I have this issue with an admin command of mine, Oath of Silence command, how it’s suppose to work is everyone in the secGroup can run except for the player’s ranked 220, and everyone in the secGroup and certain ranks in the mainGroup can see it, but I’m running into an issue of when a player is reset or on rejoin they no longer see the redacted message instead they see the normal message.

I assume this happens because I have it in StarterCharacterScript, so the information is reset when they reset/die, and my main menu forces a player to be reset to give them the tools and group based GUI. I did try to add a onCharacterAdded function to this, but it still doesn’t work. I also tried adding it to StarterPlayerScripts and this doesn’t work at all. I also tried to get AI to help, but it couldn’t fix it.

Relevant code in adminHandler(There is some stuff left out cause the admin system work and doesn’t need to be messed with):

local activeOOS = {}
local oosRemote = ReplicatedStorage.MiscEvents.OOSRemote

local GetOOSPlayers = ReplicatedStorage.MiscEvents.GetOOSPlayers
local sendOOSPlayers = ReplicatedStorage.MiscEvents.SendOOSPlayers

elseif command == "oos" then
		if not player:IsInGroup(secGroup) then return end 

		local playerRank = player:GetRankInGroup(secGroup)

		local state
		if playerRank == 220 then 
			state = true
		else 
			state = not activeOOS[player.UserId]
		end
		
		print("OOS DEBUG (COMMAND): "..player.Name.." triggered :oos. New state will be: "..tostring(state)) --<-- ADD THIS PRINT


		activeOOS[player.UserId] = state
		oosRemote:FireAllClients(player.UserId, state)
		print("OOS DEBUG (COMMAND): Fired remote event to all clients for UserId "..player.UserId) --<-- ADD THIS PRINT
		sendWebhook(player.Name, ":" .. command)
	end
end

game.Players.PlayerRemoving:Connect(function(plr)
	if activeOOS[plr.UserId] then 
		print("OOS DEBUG (LEAVING): "..plr.Name.." had active OOS. Cleaning up.") --<-- ADD THIS PRINT
		activeOOS[plr.UserId] = nil
		oosRemote:FireAllClients(plr.UserId, false)
	end
end)

local function onPlayerRejoin(player)
if player:IsInGroup(secGroup) and player:GetRankInGroup(secGroup) == 220 then 
		print("OOS DEBUG (JOIN): "..player.Name.." has Rank 220. Automatically enabling OOS.") --<-- ADD THIS PRINT
		activeOOS[player.UserId] = true
		oosRemote:FireAllClients(player.UserId, true)
		print("OOS DEBUG (JOIN): Fired remote event for "..player.Name.."'s auto-OOS.") --<-- ADD THIS PRINT

	end
	
	print("OOS DEBUG (JOIN): Checking existing OOS players to inform "..player.Name) --<-- ADD THIS PRINT
	for userId, state in pairs(activeOOS) do 
		if state then 
			print("OOS DEBUG (JOIN): Found active OOS for UserId "..tostring(userId)..". Firing event.") --<-- ADD THIS PRINT
			oosRemote:FireAllClients(userId, true)
		end
	end
end

GetOOS.OnServerInvoke = function(player)
	print("OOS SERVER: Player "..player.Name.." requested the full OOS list.")
	return activeOOS
end

game.Players.PlayerAdded:Connect(onPlayerJoin)

MessageHandler:

local TCS = game:GetService("TextChatService")
local Players = game:GetService("Players")
local rbxGeneralChannel = TCS:WaitForChild("TextChannels"):WaitForChild("RBXGeneral")

local mainGroup = 32663277 
local secGroup = 32607348
local localPlr = Players.LocalPlayer
local oosRemote = game:GetService("ReplicatedStorage").MiscEvents:WaitForChild("OOSRemote")
local getOOSPlayers = game:GetService("ReplicatedStorage").MiscEvents:WaitForChild("GetOOSPlayers")

local oosPlayers = {}
local isOOSCheckComplete = false

print("OOS CLIENT: messageHandler.lua started for " .. localPlr.Name) --<-- ADD THIS PRINT

local function onCharacterAdded(char)
	print("OOS CLIENT (CHARACTER): Character " .. char.Name .. " added.")

	if not isOOSCheckComplete then 
		local oosList = getOOSPlayers:InvokeServer()
		
		if oosList then 
			oosPlayers = oosList
			print("OOS CLIENT: Received full OOS list.")
			isOOSCheckComplete = true 
		else 
			print("OOS CLIENT: Failed to get OOS list from server.")
		end
	end
end

localPlr.CharacterAdded:Connect(onCharacterAdded)

if localPlr.Character then
	onCharacterAdded(localPlr.Character)
end

oosRemote.OnClientEvent:Connect(function(sourceUserId, state)
	print("OOS CLIENT: Received OOS remote event. UserId: "..tostring(sourceUserId).." | State: "..tostring(state)) --<-- ADD THIS PRINT
	if not sourceUserId then return end
	if state then
		oosPlayers[sourceUserId] = true
	else
		oosPlayers[sourceUserId] = nil
	end
	print("OOS CLIENT: Updated local oosPlayers table. User "..tostring(sourceUserId).." is now ".. (oosPlayers[sourceUserId] and "OOS" or "NOT OOS")) --<-- ADD THIS PRINT
end)

Players.PlayerRemoving:Connect(function(player)
	print("OOS CLIENT: Player "..player.Name.." left, removing from local oosPlayers table.") --<-- ADD THIS PRINT
	oosPlayers[player.UserId] = nil
end)

rbxGeneralChannel.OnIncomingMessage = function(textChatMessage)
	local senderSource = textChatMessage.TextSource
	if not senderSource then return nil end

	local senderId = senderSource.UserId
	print("OOS CLIENT (MESSAGE): Intercepting message from senderId: "..tostring(senderId)..". Checking OOS status...") --<-- ADD THIS PRINT
	if not oosPlayers[senderId] then
		print("OOS CLIENT (MESSAGE): Sender "..tostring(senderId).." is NOT OOS. Allowing message.") --<-- ADD THIS PRINT
		return nil
	end
	
	print("OOS CLIENT (MESSAGE): Sender "..tostring(senderId).." IS OOS. Proceeding to redact.") --<-- ADD THIS PRINT


	local textChatMsgProperties = Instance.new("TextChatMessageProperties")
	local senderPlayer = Players:GetPlayerByUserId(senderId)

	if senderPlayer and senderPlayer:IsInGroup(secGroup) then
		if localPlr:IsInGroup(secGroup) then
			return nil
		elseif localPlr:IsInGroup(mainGroup) and localPlr:GetRankInGroup(mainGroup) == 255 then
			return nil
		else
			if senderPlayer:GetRankInGroup(secGroup) == 220 then
				textChatMsgProperties.Text = "[REDACTED] - Oath of Silence"
			else
				textChatMsgProperties.Text = "[REDACTED] - On Guard Duty"
			end
			return textChatMsgProperties
		end
	end
	return nil
end

Do keep in mind, if this is a local script any local player (client) can remove the redacted message and see it, as long as the server replicates that message in plain text to the client. Meaning, a cheater could easily remove the redaction and it should be handled on the server. If this isn’t a concern here is the problem I’ve found.

The conditional logic here does not make sense given the statement you have. This says, if the player is in secGroup AND has rank 220 then you do everything.

if player:IsInGroup(secGroup) and player:GetRankInGroup(secGroup) == 220 then 

Your statement indicates that this is not the expected behavior and it should be anyone that does not equal 220.
Fixed:

if player:IsInGroup(secGroup) and player:GetRankInGroup(secGroup) ~= 220 then 

Do you know why there is a for loop after this condition? Its doing the same thing that the if condition is doing, just in a for loop?

There are quite a few inconsistencies here and I’m struggling to understand the root of the problem. Do you mind rephrasing the issue you’re having and including some of the missing code above the elseif in the first code snippet?

Well I’m new to scripting and most of the admin script was made via AI to help me understand it more, but I made the OOS command on my own untill I found this bug within it, then after like an hour of me trying to fix it, I asked AI to help me with the problems, and I jumped from one AI to another, and causes some inconsistencies with the code, but from what I understand from the AI is that For loop is meant to update all new players with the current information of the active OOS

Anything above the elseif is just more commands, the total length of the admin script is roughly 1400 lines.

This is how the command is meant to work:
Player A runs the OOS Command
Any other player that isn’t within the expection will see “[REDACTED] - On Guard Duty” or if they are ranked 220 it will auto make it so any chats from that player are sent as “[REDACTED]” - Oath of Silence"

The current issue is that when Player A runs the command, any body that is reset after that time will be able to see the unredacted command.

Edit:
I was in developer server to help figure this issue out yesterday, and a scripter had told me I would need to keep the code on the client cause of the exceptions I have with some people having to see the normal text vs the redacted text.

Haha! Welcome to AI, but good on you for using to learn rather than rely on it.

Regarding this for loop, the logic is partly there but is actually overkill and is unnecessary network traffic. What its trying to do is replicate the list of OOS players that the server has to the client. But instead of sending it to the player who just joined, it sends it to the whole server!

Instead,

local function onPlayerRejoin(player)
	if player:IsInGroup(secGroup) and player:GetRankInGroup(secGroup) == 220 then 
                -- Players with 220 & in secGroup are added to the list of activeOOS. Then all other players are notified that this player is in that list.
		activeOOS[player.UserId] = true
		oosRemote:FireAllClients(player.UserId, true)
	end
	
	for userId, state in pairs(activeOOS) do 
		if state then 
			-- And now we are only replicating this data directly to the player.
			oosRemote:FireClient(player,userId, true)
		end
	end
end

– Potential issue:
So it appears the client is checking senderPlayer:IsInGroup() from the client, this may not be replicated data. But you have a table oosPlayers which does contain that data as you replicate it from the server.

instead of using
if senderPlayer:GetRankInGroup(secGroup) == 220 then
Why not swap this to
oosPlayers[senderId]
This will redact the “senders” message. In essence, blocking anyone from seeing their message if they have 220.

I’d also delete the onCharacterAdded function on the client. You’ve already got oosPlayers stored, no need to request it again. And the server is already beaming it down immediately.

Move the player script to StartPlayerScripts, and then the server will replicate that to the client when they load in.

(It would appear that according to the documentation, IsInGroup is not up to date on the client immediately: Player | Documentation - Roblox Creator Hub)

– Network stuff:
Always think of the server as the authority in all transactions, never trust the client. Assume the client is as malicious as humany possible. If you can avoid sending excess “confidential” data to the client, avoid doing so.

But I really can’t think of any reason you couldn’t have all of your messages sent from server → client and redact it on the server.

Well I tried moving it to StarterPlayerScripts before, but once I did that the command didn’t work as in for the players currently in-game, I don’t know if it worked for any players who joined after the command was ran though.

Also on switching if senderPlayer:GetRankInGroup() part, how would I switch that since that whole part is build up with if statements? This is also one of my first times actually messing with the Text Chat system and modifying messages

So I was able to find out how to update the GetRankInGroup(), a little help from AI, and move it to the StarterPlayerScripts and I got this error

Unable to cast value to Object
Sack Begin
Script ‘SeverScriptService.AdminHandler’ , Line 1396 function OnPlayerJoin
Stack End

That error refers to the FireClient in this part of the code

for userId, state in pairs(activeOOS) do 
		if state then 
			print("OOS DEBUG (JOIN): Found active OOS for UserId "..tostring(userId)..". Firing event.") --<-- ADD THIS PRINT
			oosRemote:FireClient(userId, true)
		end
	end

fire client uses player objects,. not player user ids.

Alright I changed it to this, and it still didn’t work but didn’t give me an error message.

	for userId, state in pairs(activeOOS) do 
		if state then 
			print("OOS DEBUG (JOIN): Found active OOS for UserId "..tostring(userId)..". Firing event.") --<-- ADD THIS PRINT

			
			local plr = game.Players:GetPlayerByUserId(userId)
			if plr then 
				oosRemote:FireClient(player, true)
			end			
		end
	end