Emote Whitelist (Anti UGC emotes)

Since UGC Emotes doesnt really have much of a moderation system yet (example text) (and I doubt there will be much in the future) here is an Emote remover which uses a whitelist system based on CreatorTargetId so you can whitelist certain creators

Services that are referenced (for weakref anticheats) : AvatarEditorService & Players

Some notes

This is handled on the server to minimize stress upon roblox apis
This currently only (correctly) supports User made content (not groups) but if someone REALLY needs it to have group made content I'll add support for it

This isnt super microoptimized partly because I want it to be configurable / forkable for people and partly because I am not about to overengineer this at 1am in the morning
There is a much better way to do this
	get the remotes and check if the client cached blacklisted ids (from the server)
		a Remote system where the client invokes an array of the equipped ids (max being 8) (only if client hasnt been cached)
		server then caches that the character has been checked then the server verifies the ids (set an attribute on the HumanoidDescription or cache the instance or smthing because no GetDebugId / UniqueId :C )
		the server sends the client the blacklisted ids (only the ids the client sent if any)
	client then removes emotes with blacklisted ids
--!optimize 2
--!native

type Emote = {
	Name : string,
	IsAllowed : boolean?
}

type Emotes = {
	[number] : Emote
}

local AllowedCreators = table.freeze({ -- really shouldnt be edited after start
	1
})

local function IsntEmpty(tbl : {any}) -- sake of readablity
	return next(tbl) ~= nil
end

local function HandleEmotes(HumanoidDescription : HumanoidDescription, Emotes : Emotes)
	for Id : number, Emote : Emote in Emotes do
		if not Emote.IsAllowed then
			HumanoidDescription:RemoveEmote(Emote.Name)
			--warn("Removed Emote:", Emote.Name, `({Id})`)
		end
	end
end

-- ^^^ configs ^^^

local AvatarEditorService = game:GetService("AvatarEditorService")
local Players = game:GetService("Players")
local AllowedCache = {}

local function IsCreatorAllowed(CreatorId : number)
	return table.find(AllowedCreators, CreatorId) ~= nil
end

local function GetBatchItemDetails(AssetIds : {number})
	return pcall(AvatarEditorService.GetBatchItemDetails, AvatarEditorService, AssetIds, Enum.AvatarItemType.Asset) -- as of 8/7/2025 it does not cache
end

local function FormatEmotes(Emotes : {[string] : {number}}) -- makes it assetid : emotename
	local FormattedEmotes = {}

	for i,v in Emotes do
		local Id = v[1]

		FormattedEmotes[Id] = {
			Name = i,
			IsAllowed = AllowedCache[Id]
		}
	end

	return FormattedEmotes
end

local function CacheUnknownAssets(Emotes : Emotes) -- not handling it in here for the sake of others that want to fork
	local Assets = {}

	for i,v in Emotes do
		if v.IsAllowed == nil then
			table.insert(Assets, i)
		end
	end

	if IsntEmpty(Assets) then
		local suc, err = GetBatchItemDetails(Assets)
		if suc then
			for i,v in err do
				local AssetId = v.Id
				local IsAllowed = IsCreatorAllowed(v.CreatorTargetId)

				AllowedCache[AssetId] = IsAllowed
				Emotes[AssetId].IsAllowed = IsAllowed
			end
		else
			for i, v in Assets do
				Emotes[v].IsAllowed = true -- if the api request fails might as well just let them use the emote
			end
		end
	end
end

local function HandleHumanoidDescription(HumanoidDescription : HumanoidDescription)
	local Emotes = HumanoidDescription:GetEmotes()
	local FormattedEmotes = if IsntEmpty(Emotes) then FormatEmotes(Emotes) else nil

	if FormattedEmotes then
		CacheUnknownAssets(FormattedEmotes)
		HandleEmotes(HumanoidDescription, FormattedEmotes)
	end
end

local function RegisterCharacter(Character : Model)
	local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
	local HumanoidDescription = if Humanoid then Humanoid:FindFirstChildWhichIsA("HumanoidDescription") else nil

	if HumanoidDescription then
		HandleHumanoidDescription(HumanoidDescription)
	end
end

local function RegisterPlayer(plr : Player)
	plr.CharacterAdded:Connect(RegisterCharacter)

	if plr.Character then
		RegisterCharacter(plr.Character)
	end
end

Players.PlayerAdded:Connect(RegisterPlayer)
for i,v in Players:GetPlayers() do
	RegisterPlayer(v)
end

hope this helps

1 Like

Couldn’t get yours to work as in It removed all emotes, So I’ve modified it a bit.

local marketPlaceService = game:GetService(`MarketplaceService`)
local AvatarEditorService = game:GetService("AvatarEditorService")
local Players = game:GetService("Players")
local disallowedEmoteNames = {}

local function IsEmpty(tbl:{any}) : boolean
	if typeof(tbl)~=`table` then return false end
	return #tbl > 0
end

function HandleEmotes(HumanoidDescription : HumanoidDescription, Emotes)
	for Id : number, Emote in Emotes do

		if disallowedEmoteNames[Emote.Name] then
			pcall(HumanoidDescription.RemoveEmote, HumanoidDescription, Emote.Name)
			print("[CACHE]: Removed Emote:", Emote.Name, `({Id})`)
			continue
		end

		local success, result = pcall(marketPlaceService.GetProductInfo, marketPlaceService, Id)
		
		if success and result then
			if result.Creator.CreatorTargetId~=1 then
				print("[NO-CACHE]: Removed Emote:", Emote.Name, `({Id})`)
				disallowedEmoteNames[Emote.Name] = true
				pcall(HumanoidDescription.RemoveEmote, HumanoidDescription, Emote.Name)
			end
		elseif not success then
			warn(`[ERROR]: ({Emote.Name}) {result}`)
			pcall(HumanoidDescription.RemoveEmote, HumanoidDescription, Emote.Name)
		end
	end
end

local function FormatEquippedEmotes(desc: HumanoidDescription)
	local equipped = desc:GetEquippedEmotes()
	local equippedSet = {}

	for _, list in equipped do
		for _, emoteName in list do
			equippedSet[emoteName] = true
		end
	end

	local allEmotes = desc:GetEmotes() :: {[string]: {number}}

	local formatted: {[number]: {Name: string}} = {}

	for name, ids in allEmotes do
		if equippedSet[name] then
			for _, id in ids do
				formatted[id] = { Name = name }
			end
		end
	end

	return formatted
end

local function HandleHumanoidDescription(hd: HumanoidDescription)
	local formatted = FormatEquippedEmotes(hd)
	if not IsEmpty(formatted) then
		HandleEmotes(hd, formatted)
	end
end

local function RegisterCharacter(Character : Model)
	local Humanoid = Character:FindFirstChildWhichIsA("Humanoid")
	local HumanoidDescription = if Humanoid then Humanoid:FindFirstChildWhichIsA("HumanoidDescription") else nil

	if HumanoidDescription then
		HandleHumanoidDescription(HumanoidDescription)
	end
end

local function RegisterPlayer(plr : Player)
	plr.CharacterAdded:Connect(RegisterCharacter)

	if plr.Character then
		RegisterCharacter(plr.Character)
	end
end

Players.PlayerAdded:Connect(RegisterPlayer)

for i,v in Players:GetPlayers() do
	RegisterPlayer(v)
end

Just updated the post with the correct version (working version)
I had copied the wrong source (copied from vsc instead of Roblox Studio)
the vsc version had issues with IsEmpty logic (only supported Arrays instead of maps which can be checked with a next call) and me not passing the proper value through IsCreatorAllowed (I passed the AssetId instead of the CreatorTargetId)
My apologizes

Awesome resource! I created something similar that automatically whitelists Roblox offical emotes.

Mine already does that (ie AllowedCreators having the userid 1)

1 Like