diff --git a/NameFilter/configs/NameFilter.cfg b/NameFilter/configs/NameFilter.cfg new file mode 100644 index 00000000..8d24c314 --- /dev/null +++ b/NameFilter/configs/NameFilter.cfg @@ -0,0 +1,21 @@ +"filters" +{ + // this is what the filtered characters are replaced by, do "" to just remove them + "censor" "" + + // regex expression for filtering, matches are replaced by the censor, here we are using a whitelist - this would strip unicode characters + "filter" "[^A-Za-z0-9\s!@#$%^&*()-_+=-`~\\\]\[{}|';:/.,?><]" + + // series of regex expressions for names POST FILTER, matches will get replacement names + "banned" + { + "1" "[Aa4]+[Dd]+[Mm]+[IiL1]+[nN]+" + "2" "@((!?)me|(c?)t(s?)|(!?)admins|(!?)friends|random((c?)t?)|humans|spec|alive|dead|aim|bots)" + } + + // replacement names, granted by banned filter, or if the name is too short (<2) + "names" + { + "1" "BAD_NAME" + } +} diff --git a/NameFilter/scripting/NameFilter.sp b/NameFilter/scripting/NameFilter.sp new file mode 100644 index 00000000..49225f34 --- /dev/null +++ b/NameFilter/scripting/NameFilter.sp @@ -0,0 +1,331 @@ +#include +#include + +#pragma semicolon 1 +#pragma newdecls required + +Regex g_FilterExpr; +char g_FilterChar[2] = ""; +ArrayList g_BannedExprs; +ArrayList g_ReplacementNames; +int g_iBlockNameChangeEvents[MAXPLAYERS + 1] = {0, ...}; + +public Plugin myinfo = +{ + name = "NameFilter", + author = "BotoX", + description = "Filters player names", + version = "1.0" +} + +public void OnPluginStart() +{ + HookEvent("player_changename", Event_ChangeName, EventHookMode_Pre); + HookUserMessage(GetUserMessageId("SayText2"), UserMessage_SayText2, true); + + LoadConfig(); + + for(int i = 1; i <= MaxClients; i++) + { + if(IsClientConnected(i)) + OnClientConnected(i); + } +} + +public void OnClientConnected(int client) +{ + g_iBlockNameChangeEvents[client] = 0; + + if(IsFakeClient(client)) + return; + + char sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + if(FilterName(client, sName)) + SetClientName(client, sName); +} + +public void OnClientPutInServer(int client) +{ + if(IsFakeClient(client)) + return; + + char sName[MAX_NAME_LENGTH]; + GetClientName(client, sName, sizeof(sName)); + + if(FilterName(client, sName)) + { + g_iBlockNameChangeEvents[client] = 2; + SetClientName(client, sName); + } +} + +public Action Event_ChangeName(Event event, const char[] name, bool dontBroadcast) +{ + int client = GetClientOfUserId(event.GetInt("userid")); + if(g_iBlockNameChangeEvents[client]) + { + g_iBlockNameChangeEvents[client]--; + SetEventBroadcast(event, true); + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action UserMessage_SayText2(UserMsg msg_id, BfRead msg, const int[] players, int playersNum, bool reliable, bool init) +{ + if(!reliable) + return Plugin_Continue; + + int client; + char sMessage[32]; + char sOldName[MAX_NAME_LENGTH]; + char sNewName[MAX_NAME_LENGTH]; + + if(GetUserMessageType() == UM_Protobuf) + { + PbReadString(msg, "msg_name", sMessage, sizeof(sMessage)); + + if(!(sMessage[0] == '#' && StrContains(sMessage, "Name_Change"))) + return Plugin_Continue; + + client = PbReadInt(msg, "ent_idx"); + PbReadString(msg, "params", sOldName, sizeof(sOldName), 1); + PbReadString(msg, "params", sNewName, sizeof(sNewName), 2); + } + else + { + client = BfReadByte(msg); + BfReadByte(msg); + BfReadString(msg, sMessage, sizeof(sMessage)); + + if(!(sMessage[0] == '#' && StrContains(sMessage, "Name_Change"))) + return Plugin_Continue; + + BfReadString(msg, sOldName, sizeof(sOldName)); + BfReadString(msg, sNewName, sizeof(sNewName)); + } + + if(g_iBlockNameChangeEvents[client]) + { + g_iBlockNameChangeEvents[client]--; + return Plugin_Handled; + } + + if(FilterName(client, sNewName)) + { + if(StrEqual(sOldName, sNewName)) + { + g_iBlockNameChangeEvents[client] = 3; + SetClientName(client, sOldName); + return Plugin_Handled; + } + + g_iBlockNameChangeEvents[client] = 3; + SetClientName(client, sOldName); + + DataPack pack = new DataPack(); + pack.WriteCell(client); + pack.WriteString(sNewName); + + CreateTimer(0.1, Timer_ChangeName, pack); + + return Plugin_Handled; + } + + return Plugin_Continue; +} + +public Action Timer_ChangeName(Handle timer, any data) +{ + DataPack pack = view_as(data); + pack.Reset(); + + int client = pack.ReadCell(); + char sName[MAX_NAME_LENGTH]; + pack.ReadString(sName, sizeof(sName)); + + delete pack; + + SetClientName(client, sName); + + return Plugin_Stop; +} + +void LoadConfig() +{ + if(g_FilterExpr != INVALID_HANDLE) + CloseHandle(g_FilterExpr); + + if(g_BannedExprs) + { + for(int i = 0; i < g_BannedExprs.Length; i++) + { + Handle hRegex = g_BannedExprs.Get(i); + CloseHandle(hRegex); + } + } + + delete g_BannedExprs; + delete g_ReplacementNames; + + static char sConfigFile[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, sConfigFile, sizeof(sConfigFile), "configs/NameFilter.cfg"); + if(!FileExists(sConfigFile)) + SetFailState("Could not find config: \"%s\"", sConfigFile); + + KeyValues Config = new KeyValues("NameFilter"); + if(!Config.ImportFromFile(sConfigFile)) + { + delete Config; + SetFailState("ImportFromFile() failed!"); + } + + Config.GetString("censor", g_FilterChar, 2, "*"); + + static char sBuffer[256]; + Config.GetString("filter", sBuffer, 256); + + char sError[256]; + RegexError iError; + g_FilterExpr = CompileRegex(sBuffer, 0, sError, sizeof(sError), iError); + if(iError != REGEX_ERROR_NONE) + { + delete Config; + SetFailState(sError); + } + + g_BannedExprs = new ArrayList(); + if(Config.JumpToKey("banned")) + { + if(Config.GotoFirstSubKey(false)) + { + do + { + Config.GetString(NULL_STRING, sBuffer, sizeof(sBuffer)); + + Handle hRegex = CompileRegex(sBuffer, 0, sError, sizeof(sError), iError); + if(iError != REGEX_ERROR_NONE) + LogError("Error parsing banned filter: %s", sError); + else + g_BannedExprs.Push(hRegex); + } while(Config.GotoNextKey(false)); + Config.GoBack(); + } + Config.GoBack(); + } + + g_ReplacementNames = new ArrayList(ByteCountToCells(MAX_NAME_LENGTH)); + if(Config.JumpToKey("names")) + { + if(Config.GotoFirstSubKey(false)) + { + do + { + Config.GetString(NULL_STRING, sBuffer, sizeof(sBuffer)); + + g_ReplacementNames.PushString(sBuffer); + } while(Config.GotoNextKey(false)); + Config.GoBack(); + } + Config.GoBack(); + } + + if(!g_ReplacementNames.Length) + { + LogError("Warning, you didn't specify any replacement names!"); + g_ReplacementNames.PushString("BAD_NAME"); + } + + delete Config; +} + +bool FilterName(int client, char[] sName, int Length = MAX_NAME_LENGTH) +{ + bool bChanged = false; + RegexError iError; + + // SourceMod Regex bug + int Guard; + for(Guard = 0; Guard < 100; Guard++) + { + int Match = MatchRegex(g_FilterExpr, sName, iError); + if(iError != REGEX_ERROR_NONE) + { + LogError("Regex Error: %d", iError); + break; + } + + if(Match <= 0) + break; + + for(int i = 0; i < Match; i++) + { + char sMatch[MAX_NAME_LENGTH]; + if(GetRegexSubString(g_FilterExpr, i, sMatch, sizeof(sMatch))) + { + if(ReplaceStringEx(sName, Length, sMatch, g_FilterChar) != -1) + bChanged = true; + } + } + } + if(Guard == 100) + LogError("SourceMod Regex failed! \"%s\"", sName); + + if(g_BannedExprs) + { + for(int i = 0; i < g_BannedExprs.Length; i++) + { + Handle hRegex = g_BannedExprs.Get(i); + int Match = MatchRegex(hRegex, sName, iError); + if(iError != REGEX_ERROR_NONE) + { + LogError("Regex Error: %d", iError); + continue; + } + + if(Match <= 0) + continue; + + int RandomName = client % g_ReplacementNames.Length; + g_ReplacementNames.GetString(RandomName, sName, Length); + return true; + } + } + + if(bChanged) + { + if(strlen(sName) < 2) + { + int RandomName = client % g_ReplacementNames.Length; + g_ReplacementNames.GetString(RandomName, sName, Length); + return true; + } + + TerminateNameUTF8(sName); + } + + return bChanged; +} + +// ensures that utf8 names are properly terminated +stock void TerminateNameUTF8(char[] name) +{ + int len = strlen(name); + for(int i = 0; i < len; i++) + { + int bytes = IsCharMB(name[i]); + if(bytes > 1) + { + if(len - i < bytes) + { + name[i] = '\0'; + return; + } + + i += bytes - 1; + } + } +}