summaryrefslogtreecommitdiff
path: root/nameblocker.sp
blob: ebfd116dc7a7082255b3ed61835f5f54d04392d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#pragma newdecls required

#include <sourcemod>
#include <regex>
#include <convars>

public Plugin myinfo = {
    name            = "SM Name Blocker",
    description     = "A simple plugin to stop people with blacklisted names from joining a server",
    author          = "NW/RL",
    version         = "alpha-0.1",
    url             = "git.dabikers.online/smnameblocker"
};

// Planned features:
    // Soft-ban: Kick players with offending names, but don't ban them. A name change will prevent the blocking
    // Hard-ban: Ban players with offending names
    // Moderation commands: A set of admin commands to add, modify, or remove name filters

enum {
    OPMODE_UNDEF,
    
    OPMODE_DISABLED,
    OPMODE_KICK,
    OPMODE_BAN,

    OPMODE_TOOBIG
};

static const int DEFAULT_OPERATING_MODE     = OPMODE_KICK;
static const int DEFAULT_REGEX_FLAGS        = (PCRE_CASELESS | PCRE_EXTENDED | PCRE_NOTEMPTY | PCRE_UTF8);
static const int DEFAULT_ADMIN_IMMUNITY     = ADMFLAG_GENERIC;
static const char DEFAULT_FAILED_NAME_MSG[] = "Failed name check";

ConVar nb_cvarOperatingMode;        static const char nb_cvarOM[]       = "nb_OPERATINGMODE";
ConVar nb_cvarRegexFlags;           static const char nb_cvarDRFNAME[]  = "nb_REGEXFLAGS";
ConVar nb_cvarAdminImmunity;        static const char nb_cvarDAINAME[]  = "nb_ADMINIMMUNITY";
ConVar nb_cvarFailedNameMessage;    static const char nb_cvarFNMNAME[]  = "nb_FAILEDNAMEMSG";

ArrayList nb_filterlist;

public void OnPluginStart() {
    // Initialize data types
    nb_filterlist = new ArrayList();

    // Read config and populate filterlist
        // TODO: This

    // Register ConVars
    char tmp[4 + 1]; IntToString(DEFAULT_OPERATING_MODE, tmp, sizeof(tmp));
    nb_cvarOperatingMode = CreateConVar(nb_cvarOM, tmp, "Operating mode", 0, true, 0.0, true, 2.0);
    nb_cvarOperatingMode.SetInt(DEFAULT_OPERATING_MODE);

    IntToString(DEFAULT_REGEX_FLAGS, tmp, sizeof(tmp));
    nb_cvarRegexFlags = CreateConVar(nb_cvarDRFNAME, tmp, "Regex compilation flags");
    nb_cvarRegexFlags.SetInt(DEFAULT_REGEX_FLAGS);

    IntToString(DEFAULT_ADMIN_IMMUNITY, tmp, sizeof(tmp));
    nb_cvarAdminImmunity = CreateConVar(nb_cvarDAINAME, tmp, "Admin immunity flags");
    nb_cvarAdminImmunity.SetInt(DEFAULT_ADMIN_IMMUNITY);

    nb_cvarFailedNameMessage = CreateConVar(nb_cvarFNMNAME, DEFAULT_FAILED_NAME_MSG, "Message delivered to users when failing a name check");
    nb_cvarFailedNameMessage.SetString(DEFAULT_FAILED_NAME_MSG);

    AutoExecConfig(true, "nameblocker_cvars");

    // Register admin commands
    RegAdminCmd("nb_addpattern", nb_addpattern, ADMFLAG_BAN, "Add a regex pattern to the filter-list");
    RegAdminCmd("nb_delpattern", nb_delpattern, ADMFLAG_BAN, "Remove a regex pattern from the filter-list");
    RegAdminCmd("nb_modpattern", nb_modpattern, ADMFLAG_BAN, "Modify (replace) an existing regex pattern in the filter-list");

    return;
}

public void OnClientPostAdminCheck(int client) {
    checkName(client);

}

public void OnClientSettingsChanged(int client) {
    // I'm not sure exactly what settings changes cause this to fire, but supposedly a name change would cause this to run, if
    // that one user on AlliedForums is correct about OnClientSettingsChanged being Sourcemod's implementation of the player_info
    // event. If this doesn't work, I can hook player_changename or player_info messages, and barring that I can still just poll 
    // everyone's names every minute or so
    
    // Also, if checkName becomes too expensive, I'll create an adt_trie to keep track of users via the GetClientSerial and 
    // GetClientName functions (<K:V> -> <Serial:Name>)

    checkName(client);
}

void checkName(int client) {
    // Skip if checking is turned off
    if(GetConVarInt(nb_cvarOperatingMode) <= OPMODE_DISABLED)
        return;

    // Don't bother checking / kicking the client if they're an admin / meet the immunity convar
    if(CheckCommandAccess(client, "", GetConVarInt(nb_cvarAdminImmunity), true))
        return;
    
    char name[32 + 1];
    GetClientName(client, name, sizeof(name));
    if(strlen(name) <= 0) {
        LogError("Tried getting a client's name and couldn't for some reason");
        return;
    }

    for(int i = 0; i < nb_filterlist.Length; i++) {
        Regex cur = nb_filterlist.Get(i); RegexError reerr;
        if(MatchRegex(cur, name, reerr) > 0)
            handleNameFail(client);
    }

    return;
}

void handleNameFail(int client) {
    int mode = GetConVarInt(nb_cvarOperatingMode);
    switch(mode) {
        case OPMODE_KICK: {
            char failmsg[512 + 1];
            GetConVarString(nb_cvarFailedNameMessage, failmsg, sizeof(failmsg));
            KickClient(client, failmsg);
        }

        case OPMODE_BAN: {
            // TODO: Figure out how to interface with other banning tools
        }

        default: {
            LogError("Tried to handle a failed name, but operating mode is invalid")
            return;
        }
    }

    return;
}

public Action nb_addpattern(int client, int args) {
    if(args < 1) {
        ReplyToCommand(client, "Error: No pattern specified");
        return Plugin_Handled;
    }

    char pattern[512 + 1];
    GetCmdArg(1, pattern, sizeof(pattern));
    if(strlen(pattern) <= 0) {
        ReplyToCommand(client, "Error: Pattern is null or empty");
        return Plugin_Handled;
    }

    int reflags = GetConVarInt(nb_cvarRegexFlags);
    if(args < 2) {
        char flagholder[1 + 1]; // "Array", lol
        GetCmdArg(2, flagholder, sizeof(flagholder));
        reflags = StringToInt(flagholder, 2);
    }

    RegexError reerr = REGEX_ERROR_NONE; Regex re = null; char errbuf[512 + 1];
    re = CompileRegex(pattern, reflags, errbuf, sizeof(errbuf), reerr);
    if(re == INVALID_HANDLE) {
        ReplyToCommand(client, "Error: Could not compile pattern: %s (%d)", errbuf, reerr);
        return Plugin_Handled;
    }

    nb_filterlist.Push(re);

    char id[8 + 1];
    GetClientAuthId(client, AuthId_SteamID64, id, sizeof(id)); id[8] = '\0';
    LogAction(client, -1, "Added %s to nameblocker filter list. (Added by: %s)", pattern, id);
    ShowActivity2(client, "NameBlocker:", "Added %s to nameblocker filter list", pattern);

    return Plugin_Handled;
}

public Action nb_delpattern(int client, int args) {
    if(args < 1) {
        ReplyToCommand(client, "Error: No pattern targeted");
        return Plugin_Handled;
    }

    // Get index via GetCmdArg
    // Check if it's valid
    // Remove it from the filter-list

    int index = GetCmdArgInt(1);
    if(index < 0 || index >= nb_filterlist.Length) {
        ReplyToCommand(client, "Error: Index out of range. Expected: [0, %d], Got: %d", (nb_filterlist.Length - 1), index);
        return Plugin_Handled;
    }

    char id[8];
    GetClientAuthId(client, AuthId_SteamID64, id, sizeof(id));
    LogAction(client, -1, "Removed pattern #%d from filter list. (Removed by: %s)", index, id);
    ShowActivity2(client, "NameBlocker:", "Removed pattern #%d from filter list", index);
    CloseHandle(nb_filterlist.Get(index)); // Not sure if this is needed. (Source)Pawn is not a garbage collected language, so dropping handles should be bad, but I'm not certain whether CompileRegex allocates memory or not
    nb_filterlist.Erase(index);

    return Plugin_Handled;
}

public Action nb_modpattern(int client, int args) {
    if(args < 2) {
        if(args < 1)
            ReplyToCommand(client, "Error: No pattern targeted for modification");
        else
            ReplyToCommand(client, "Error: No new pattern specified");
        
        return Plugin_Handled;
    }

    // Get arguments and check if they're valid
    // Replace regex handle at index with new regex

    return Plugin_Handled;
}