summaryrefslogtreecommitdiff
path: root/nameblocker.sp
blob: c6686c22211fcd7df9becf85afd29891c9ddd65a (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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
// Notes:
    // Memory allocated via the "new" keyword IS garbage collected. Handles are not. Close handles when you're done with them

    // Handles are closed when the plugin is unloaded, so if the lifetime of a handle is the plugin's lifetime, don't worry about
        // closing it

    // The contents of ArrayList objects are lost on SERVER RESTART ONLY. Any persistent data should be stored in a key-value file
        // OR in a database

    // Basically every string function will add a null terminator, so add `+ 1` to the end of any defined size string definition

    // It is possible to throw an error with the ThrowError function, but anything that would bother should probably disable the
        // plugin entirely, so SetFailState is better

    // Large local var allocations if done commonly should be made static. Increases baseline memory usage, but lowers time spent
        // allocating memory. That being said, don't bother unless it's actually a problem. Name checks should be relatively
        // infrequent, and everything else exponentially less frequent. Dropping a frame or 2 to allocate 2048 bytes of memory
        // every century shouldn't be the end of the world

    // The database should contain:
        // The string used to compile a regex pattern, sanitized to avoid sql injection attacks
        // The steamid of the admin who added a regex pattern
        // The date (YYYY-MM-DD) a regex pattern was added
        // The time (HH:MM:SS.SSS) a regex pattern was added

        // In the future, this may change to include regex compilation flags, kick/ban modes, etc. . Depends on how many features
        // I want to cram into this given how fundamentally shitty SourcePawn as a language is. I'm not trying to keep track of 17
        // different arrays and make sure that the indicies between them never get out of date. Maybe enum structs will solve my
        // concerns, maybe not

    // Plugins are loaded/unloaded on server start/end, and on map changes IF the plugin has been modified. This means, because I'm
        // first writing to the database and then updating the lists, there realistically shouldn't be any weird memory problems.
        // That makes this a whole lot easier

    // If database locking takes too long I might need to use threaded calls, which sounds like a fucking nightmare

#pragma newdecls required
#pragma semicolon 1

#include <sourcemod>
#include <regex>

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"
};

// Size of a handle, because the actual sizeof() function/macro doesn't work on "scalar"s (which is fucking stupid because I'm asking for the literal size of the variable, not the length. If I wanted length, I'd use something like lengthof)
#define HANDLE_SIZE (32)

// The literal character width of a steamid in 64bit representation. Not necessarily large enough to store a steamid64 in a string
#define LITERALSTEAMID64LENGTH (17)
// Length of a steamid64, suitably large enough to store in a string
#define STEAMID64LENGTH (LITERALSTEAMID64LENGTH + 1)

#define PATTERN_MAX_LEN (512 + 1) /* 512 chars + null terminator */
#define DATABASE_FAIL_MSG ("Could not populate regex & pattern lists from database")

enum OperatingMode {
    OP_DISABLED,
    OP_KICK,
    OP_BAN,
    OP_TOOBIG
}

ArrayList regexlist;
ArrayList patternlist;

#define DBTABLENAME         "\"163687013_SMNameBlocker\""
#define DBCREATETABLE       "CREATE TABLE IF NOT EXISTS " ... DBTABLENAME ... " (id INTEGER NOT NULL, regexstr TEXT NOT NULL ON CONFLICT IGNORE, steamid64 TEXT NOT NULL ON CONFLICT IGNORE, dateof TEXT DEFAULT CURRENT_DATE, timeof TEXT DEFAULT CURRENT_TIME, PRIMARY KEY (id), UNIQUE (regexstr));"
#define DBINSERTSTATEMENT   "INSERT OR IGNORE INTO " ... DBTABLENAME ... " (regexstr, steamid64) VALUES (?, ?);"
#define DBDELETESTATEMENT   "DELETE FROM " ... DBTABLENAME ... " WHERE regexstr=?;"
#define DBREPLACESTATEMENT  "UPDATE OR IGNORE " ... DBTABLENAME ... " SET regexstr=?, steamid64=? WHERE regexstr=?;"
#define DBPOPULATESTATEMENT "SELECT regexstr FROM " ... DBTABLENAME ... ";"

Database db;
DBStatement dbInsert;
DBStatement dbDelete;
DBStatement dbReplace;
DBStatement dbPopulate;


ConVar gcvarOperMode;       static const char OPERMODENAME[]            = "nameblock_OperatingMode";            const OperatingMode DEFAULTOPERMODE = OP_KICK;
ConVar gcvarAdmCmdFlag;     static const char ADMCMDFLAGNAME[]          = "nameblock_AdminCommandFlag";         const int DEFAULTADMCMDFLAG = ADMFLAG_BAN;
ConVar gcvarRegexCompFlags; static const char REGEXCOMPFLAGSNAME[]      = "nameblock_RegexCompilationFlags";    const int DEFAULTREGEXCOMPFLAGS = (PCRE_CASELESS | PCRE_DOTALL | PCRE_EXTENDED | PCRE_UTF8);
ConVar gcvarAmdImmFlag;     static const char ADMINIMMUNITYFLAGNAME[]   = "nameblock_AdminImmunityFlag";        const int DEFAULTADMIMMFLAG = ADMFLAG_GENERIC;
ConVar gcvarBanTime;        static const char BANTIMENAME[]             = "nameblock_BanTime";                  const int DEFAULTBANTIME = 10080; // 1 week in minutes
ConVar gcvarKickMsg;        static const char KICKMSGNAME[]             = "nameblock_KickMessage";              static const char DEFAULTKICKMSG[] = "Failed name check";
ConVar gcvarBanMsg;         static const char BANMSGNAME[]              = "nameblock_BanMessage";               static const char DEFAULTBANMESSAGE[] = "Failed name check";

// Logs and throws an error with the same message of upto 2048 characters
void logAndFail(const char[] format, any ...) {
    char buf[2048 + 1];
    VFormat(buf, sizeof(buf), format, 2);

    LogError(buf);
    SetFailState(buf);
}

// Concatenate arguments into a single buffer
int concatArgs(char[] buf, int maxbuflen, int maxarglen, int end, int start=1) {
    if(maxbuflen <= 0 || start < 0) return -1;

    char[] arg = new char[maxarglen];
    char[] tmp = new char[maxbuflen];

    GetCmdArg(start, tmp, maxarglen); // Priming the first argument to prevent weirdness
    for(int i = start + 1; i <= end; i++) {
        GetCmdArg(i, arg, maxarglen);
        Format(tmp, maxbuflen, "%s %s", tmp, arg);
    }

    return strcopy(buf, maxbuflen, tmp);
}

// Register a convar who's default value is an integer
int registerIntConVar(ConVar& cv, int defaultVal, const char[] name, const char[] desc) {
    char tmp[32]; // Consider this a byte array
    Format(tmp, sizeof(tmp), "%d", defaultVal);

    cv = CreateConVar(name, tmp, desc);
    if(cv == null) return -1;
    cv.IntValue = defaultVal;

    return 0;
}

// `registerIntConVar` wrapper. Calls `logAndFail` if the convar couldn't be registered
void xRegisterIntConVar(ConVar& cv, int defaultVal, const char[] name, const char[] desc) {
    if(registerIntConVar(cv, defaultVal, name, desc)) logAndFail("Could not register cvar \"%s\"", name);
}



void initDatabase() {
    // Get database handle
    char sqlerr[256 + 1];
    bool err = false;

    db = SQLite_UseDatabase("sourcemod-local", sqlerr, sizeof(sqlerr));
    if(db == null) logAndFail("Could not connect to sql database: %s", sqlerr);

    // Prep table
    SQL_LockDatabase(db);
    if(!SQL_FastQuery(db, DBCREATETABLE)) {
        SQL_GetError(db, sqlerr, sizeof(sqlerr));
        err = true;
    }
    SQL_UnlockDatabase(db);

    if(err) logAndFail("Could not create table: %s", sqlerr);

    return;
}

void initPrepStatements() {
    char sqlerr[256 + 1];
    int err = 0;

    SQL_LockDatabase(db);
    if((dbInsert = SQL_PrepareQuery(db, DBINSERTSTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err)
        err = 1;

    if((dbDelete = SQL_PrepareQuery(db, DBDELETESTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err)
        err = 2;

    if((dbReplace = SQL_PrepareQuery(db, DBREPLACESTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err)
        err = 3;

    if((dbPopulate = SQL_PrepareQuery(db, DBPOPULATESTATEMENT, sqlerr, sizeof(sqlerr))) == null && !err)
        err = 4;

    SQL_UnlockDatabase(db);
    switch(err) {
        case 1: {logAndFail("Could not prepare insert statement: %s",   sqlerr);} // Doesn't recognize "163687013_SMNameBlocker" as a valid token
        case 2: {logAndFail("Could not prepare delete statement: %s",   sqlerr);}
        case 3: {logAndFail("Could not prepare replace statement: %s",  sqlerr);}
        case 4: {logAndFail("Could not prepare populate statement: %s", sqlerr);}
    }

    return;
}

void loadFromDatabase() {
    // Initialize and populate datatypes
    regexlist = new ArrayList(ByteCountToCells(HANDLE_SIZE));
    if(regexlist == null) logAndFail("Could not initialize regexlist ArrayList");

    patternlist = new ArrayList(ByteCountToCells(PATTERN_MAX_LEN));
    if(patternlist == null) logAndFail("Could not initialize patternlist ArrayList");

    // Initialize table if it doesn't exist
    int err = 0;
    char sqlerr[256 + 1];

    SQL_LockDatabase(db);

    // select patterns from nameblock table/database
    if(SQL_Execute(dbPopulate) && !err) {
        SQL_GetError(dbPopulate, sqlerr, sizeof(sqlerr));
        err = 2;
    }

    SQL_UnlockDatabase(db);
    switch(err) {
        case 1: {logAndFail("Could not initialize nameblocker table: %s", sqlerr);}
        case 2: {logAndFail("Population query failed: %s", sqlerr);}
    }


    // compile each pattern & insert
    Regex cur; char reerr[256]; RegexError reenum;
    for(char pattern[PATTERN_MAX_LEN]; SQL_FetchRow(dbPopulate);) {
        SQL_FetchString(dbPopulate, 1, pattern, sizeof(pattern));
        cur = CompileRegex(pattern, gcvarRegexCompFlags, reerr, sizeof(reerr), reenum);
        if(cur == null) {
            LogError("Could not compile regex pattern \"%s\": %s (%d)", pattern, reerr, reenum);
            continue;
        }

        if(aModPattern(MM_INSERT, regexlist.Length, pattern)) {
            LogError("Couldn't add regex \"%s\" to arraylists, continuing", pattern);
            CloseHandle(cur);
        }
    }

    return;
}



public void OnAllPluginsLoaded() {
    initDatabase();
    initPrepStatements();
    loadFromDatabase();

    // Register convars
    xRegisterIntConVar(gcvarOperMode,       DEFAULTOPERMODE,        OPERMODENAME,           "Operating mode (disabled, kick, ban, etc.)");
    xRegisterIntConVar(gcvarAdmCmdFlag,     DEFAULTADMCMDFLAG,      ADMCMDFLAGNAME,         "Admin flag to modify pattern list");
    xRegisterIntConVar(gcvarRegexCompFlags, DEFAULTREGEXCOMPFLAGS,  REGEXCOMPFLAGSNAME,     "Regular expression compilation flags");
    xRegisterIntConVar(gcvarAmdImmFlag,     DEFAULTADMIMMFLAG,      ADMINIMMUNITYFLAGNAME,  "Admin immunity flag");
    xRegisterIntConVar(gcvarBanTime,        DEFAULTBANTIME,         BANTIMENAME,            "Time (in minutes) to ban players when operating in ban mode. 0 for permaban");

    gcvarKickMsg = CreateConVar(KICKMSGNAME, DEFAULTKICKMSG, "Default kick message");
    if(gcvarKickMsg == null) logAndFail("Could not init convar \"gcvarKickMsg\"");

    gcvarBanMsg = CreateConVar(BANMSGNAME, DEFAULTBANMESSAGE, "Default ban message");
    if(gcvarBanMsg == null) logAndFail("Could not init convar \"gcvarBanMsg\"");

    AutoExecConfig(true, "nameblocker_cvars");

    // Register commands
    RegAdminCmd("nb_addpattern",    cmdRegisterPattern,    gcvarAdmCmdFlag.IntValue, "Add a regex pattern to the filter list");
    RegAdminCmd("nb_removepattern", cmdDeletePattern,      gcvarAdmCmdFlag.IntValue, "Remove a regex pattern from the filter list");
    RegAdminCmd("nb_modifypattern", cmdReplacePattern,     gcvarAdmCmdFlag.IntValue, "Replace an existing regex pattern with a new pattern");
    RegAdminCmd("nb_listpatterns",  cmdListPatterns,       gcvarAdmCmdFlag.IntValue, "List current regex patterns and their indicies");
}

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

}

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

}



public Action cmdRegisterPattern(int client, int args) {
    if(args < 2) {
        ReplyToCommand(client, "Error: missing regex pattern");
        return Plugin_Handled;
    }

    char pattern[PATTERN_MAX_LEN];
    if(concatArgs(pattern, sizeof(pattern), PATTERN_MAX_LEN, args) <= 0) {
        ReplyToCommand(client, "Error: could not get argument list");
        return Plugin_Handled;
    }

    if(modPattern(MM_INSERT, regexlist.Length, pattern)) {
        ReplyToCommand(client, "Error: could not register pattern");
        return Plugin_Handled;
    }

    return Plugin_Handled;
}

public Action cmdDeletePattern(int client, int args) {
    if(args < 2) {
        ReplyToCommand(client, "Error: no pattern index given");
        return Plugin_Handled;
    }

    int index;
    if(!GetCmdArgIntEx(1, index)) {
        ReplyToCommand(client, "Error: index argument not numerical");
        return Plugin_Handled;
    }

    if(modPattern(MM_DELETE, index)) {
        ReplyToCommand(client, "Error: could not remove pattern");
        return Plugin_Handled;
    }

    return Plugin_Handled;
}

public Action cmdReplacePattern(int client, int args) {
    if(args < 3) {
        ReplyToCommand(client, "Error: missing index, replacement pattern, or both");
        return Plugin_Handled;
    }

    int index;
    if(!GetCmdArgIntEx(1, index)) {
        ReplyToCommand(client, "Error: index argument not numerical");
        return Plugin_Handled;
    }

    char pattern[PATTERN_MAX_LEN];
    if(concatArgs(pattern, sizeof(pattern), PATTERN_MAX_LEN, args, 2) <= 0) {
        ReplyToCommand(client, "Error: could not get argument list");
        return Plugin_Handled;
    }

    if(modPattern(MM_REPLACE, index, pattern)) {
        ReplyToCommand(client, "Error: could not replace pattern in list");
        return Plugin_Handled;
    }

    return Plugin_Handled;
}

public Action cmdListPatterns(int client, int args) {
    for(int i = 0; i < patternlist.Length; i++) {
        ReplyToCommand(client, "[%d] %s", i, patternlist.Get(i));
    }

    return Plugin_Handled;
}



enum MOD_MODE {
    MM_UNDEF,
    MM_DELETE,
    MM_INSERT,
    MM_REPLACE,
    MM_TOOBIG
};

int __modPattern__insert(int index, char[] pattern, int patternlen, int client) {
    if(IsNullString(pattern) || patternlen < 0 || patternlen > PATTERN_MAX_LEN || client < 0) {return -1;}

    char steamid64[STEAMID64LENGTH];
    char sqlerr[512 + 1];
    bool flag = false;

    if(!GetClientAuthId(client, AuthId_SteamID64, steamid64, sizeof(steamid64))) {
        LogError("Could not get client \"%N\"'s steamid64", client);
        return -1;
    }


    SQL_LockDatabase(db);

    SQL_BindParamString(dbInsert, 0, pattern, false);
    SQL_BindParamString(dbInsert, 1, steamid64, false);
    if(!SQL_Execute(dbInsert)) {
        SQL_GetError(dbInsert, sqlerr, sizeof(sqlerr));
        LogError("Could not insert values into database: %s", sqlerr);
        flag = true;
    }

    SQL_UnlockDatabase(db);

    if(flag)
        return -1;

    if(aModPattern(MM_INSERT, index, pattern)) {} // TODO: Error handling

    return 0;
}
int __modPattern__delete(int index) {
    char pattern[PATTERN_MAX_LEN];
    char sqlerr[512 + 1];
    bool flag = false;

    patternlist.GetString(index, pattern, sizeof(pattern), ByteCountToCells(PATTERN_MAX_LEN));

    SQL_LockDatabase(db);

    SQL_BindParamString(dbDelete, 0, pattern, false);
    if(!SQL_Execute(dbDelete)) {
        SQL_GetError(dbDelete, sqlerr, sizeof(sqlerr));
        LogError("Could not delete from database: %s", sqlerr);
        flag = true;
    }

    SQL_UnlockDatabase(db);

    if(flag)
        return -1;

    if(aModPattern(MM_DELETE, index)) {} // TODO: Error handling

    return 0;
}
int __modPattern__replace(int index, char[] pattern, int patternlen, int client) {
    if(IsNullString(pattern) || patternlen < 0 || patternlen > PATTERN_MAX_LEN || client < 0) {return -1;}

    char oldpattern[PATTERN_MAX_LEN];
    char steamid64[STEAMID64LENGTH];
    char sqlerr[512];
    bool flag = false;

    patternlist.GetString(index, oldpattern, sizeof(oldpattern), ByteCountToCells(PATTERN_MAX_LEN));
    if(!GetClientAuthId(client, AuthId_SteamID64, steamid64, sizeof(steamid64))) {
        LogError("Could not get \"%N\"'s steamid64");
        return -1;
    }

    SQL_LockDatabase(db);

    SQL_BindParamString(dbReplace, 0, pattern, false);
    SQL_BindParamString(dbReplace, 1, steamid64, false);
    SQL_BindParamString(dbReplace, 2, oldpattern, false);

    if(!SQL_Execute(dbReplace)) {
        SQL_GetError(dbReplace, sqlerr, sizeof(sqlerr));
        LogError("Could not replace in database: %s", sqlerr);
        flag = true;
    }

    SQL_UnlockDatabase(db);

    if(flag)
        return -1;

    if(aModPattern(MM_REPLACE, index, pattern)) {} // TODO: Error handling

    return 0;
}

int modPattern(MOD_MODE mode, int index, char[] pattern="", int patternlen=-1, int client=-1) {
    if(index < 0 || index > regexlist.Length) return -1;

    switch(mode) {
        case MM_INSERT:     {return __modPattern__insert(index, pattern, patternlen, client);}
        case MM_DELETE:     {return __modPattern__delete(index);}
        case MM_REPLACE:    {return __modPattern__replace(index, pattern, patternlen, client);}
        default: {
            LogError("Given invalid DBMOD_MODE");
            return -1;
        }
    }
}

int aModPattern(MOD_MODE mode, int index, char[] pattern="", int patternlen=0) {
    if(mode <= MM_UNDEF || mode >= MM_TOOBIG || index < 0 || index > patternlist.Length) return -1;

    Regex res;
    if(mode > MM_DELETE) {
        if(IsNullString(pattern) || patternlen < 0 || patternlen > PATTERN_MAX_LEN) return -1;

        char errstr[512]; RegexError reerr;
        res = CompileRegex(pattern, gcvarRegexCompFlags.IntValue, errstr, sizeof(errstr), reerr);
        if(res == null) {
            LogError("Error: Could not compile regex pattern \"%s\": %s (%d)", pattern, errstr, reerr);
            return -1;
        }
    }

    switch(mode) {
        case MM_DELETE: {
            regexlist.Erase(index);
            patternlist.Erase(index);

        }

        case MM_INSERT: {
            if(index == regexlist.Length) {
                regexlist.Push(res);
                patternlist.Push(pattern);
                return 0;
            }

            regexlist.ShiftUp(index);
            patternlist.ShiftUp(index);
            regexlist.Set(index, res);
            patternlist.Set(index, pattern);
        }

        case MM_REPLACE: {
            if(index == regexlist.Length) {
                regexlist.Push(res);
                patternlist.Push(pattern);
                return 0;
            }

            CloseHandle(view_as<Handle>(regexlist.Get(index, ByteCountToCells(HANDLE_SIZE))));
            patternlist.Set(index, pattern, ByteCountToCells(PATTERN_MAX_LEN));
            regexlist.Set(index, res, ByteCountToCells(HANDLE_SIZE));
        }


        default: {
            logAndFail("Got impossible state");
        }
    }

    return 0;
}




// Check's a user's name against the regex list. Returns -1 on error, 0 if skipped, 1 on hit
int checkName(int client) {
    if(client <= 0) return 0;
    if(gcvarOperMode.IntValue == view_as<int>(OP_DISABLED)) return 0;
    if(CheckCommandAccess(client, "", gcvarAmdImmFlag.IntValue, true)) return 0;

    char name[64 + 1];
    if(getName(client, name, sizeof(name)) <= 0) {
        LogError("Tried to get a client's name for a name check, but could not");
        return -1;
    }

    RegexError reerr;
    for(int i = 0, m = 0; i < regexlist.Length; i++) {
        m = MatchRegex(regexlist.Get(i), name, reerr);

        if(m < 0) {
            handleFailedRegex(client);
            return -1;
        }
        if(m == 0) continue;

        handleNameHit(client);
        break;
    }

    return 1;
}

int getName(int client, char[] buf, int buflen) {
    if(client <= 0 || buflen <= 0) return -1;
    if(IsNullString(buf)) return -1;

    return Format(buf, buflen, "%N", client);
}

int handleNameHit(int client) {
    if(client <= 0) return -1;

    switch(gcvarOperMode.IntValue) {
        case OP_DISABLED: {return 0;}
        case OP_KICK: {
            char kickmsg[512];
            gcvarKickMsg.GetString(kickmsg, sizeof(kickmsg));

            KickClient(client, kickmsg);
            LogAction(0, client, "Kicked %L for failing a name check", client);
            return 0;
        }
        case OP_BAN: {
            char banmsg[512];
            gcvarBanMsg.GetString(banmsg, sizeof(banmsg));

            BanClient(client, gcvarBanTime.IntValue, BANFLAG_AUTHID, "Failed name check", banmsg);
            LogAction(0, client, "Banned %L for failing a name check", client);

            // TODO: Interop with other ban systems
            return 0;
        }
        default: {
            LogError("%L failed a name check, but the operating mode in an invalid state", client);
            return -1;
        }
    }
}

int handleFailedRegex(int client) {
    LogError("Ran into regex error when trying to check user %L's name", client);
    return 0;
}