/* * CSB Ban Manager * Copyright (C) 2026 counter-strike-boost.com * * MySQL-backed ban system for AMX Mod X using threaded SQLx queries, so the * game thread is never blocked by the database. * * Inspired by AMXBans (originally by Kanagava / the AMXBans team). This is an * independent GPL re-implementation with its own, much smaller schema; no code * from AMXBans is reused. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. It is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details: . */ #include #include #include new const PLUGIN[] = "CSB Ban Manager" new const VERSION[] = "1.0.0" new const AUTHOR[] = "counter-strike-boost.com" #define TABLE_NAME "csb_bans" new Handle:g_hTuple = Empty_Handle new g_pHost, g_pUser, g_pPass, g_pDb, g_pKickMsg public plugin_init() { register_plugin(PLUGIN, VERSION, AUTHOR) g_pHost = register_cvar("csb_ban_host", "127.0.0.1") g_pUser = register_cvar("csb_ban_user", "csb") g_pPass = register_cvar("csb_ban_pass", "") g_pDb = register_cvar("csb_ban_db", "csb") g_pKickMsg = register_cvar("csb_ban_kickmsg", "You are banned on this server") register_concmd("amx_ban", "cmdBan", ADMIN_BAN, " [reason]") register_concmd("amx_unban", "cmdUnban", ADMIN_BAN, "") register_concmd("amx_bans", "cmdBans", ADMIN_BAN, "- shows the 20 most recent bans in a MOTD") } public plugin_cfg() { new host[64], user[64], pass[64], db[64] get_pcvar_string(g_pHost, host, charsmax(host)) get_pcvar_string(g_pUser, user, charsmax(user)) get_pcvar_string(g_pPass, pass, charsmax(pass)) get_pcvar_string(g_pDb, db, charsmax(db)) g_hTuple = SQL_MakeDbTuple(host, user, pass, db) new query[512] formatex(query, charsmax(query), "CREATE TABLE IF NOT EXISTS `%s` (`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `authid` VARCHAR(32) NOT NULL, `ip` VARCHAR(24) NOT NULL DEFAULT '', `player` VARCHAR(32) NOT NULL DEFAULT '', `admin` VARCHAR(32) NOT NULL DEFAULT '', `admin_authid` VARCHAR(32) NOT NULL DEFAULT '', `reason` VARCHAR(128) NOT NULL DEFAULT '', `created` INT UNSIGNED NOT NULL DEFAULT 0, `expires` INT UNSIGNED NOT NULL DEFAULT 0, KEY `k_authid` (`authid`), KEY `k_ip` (`ip`)) ENGINE=InnoDB", TABLE_NAME) SQL_ThreadQuery(g_hTuple, "handlerSimple", query) } public plugin_end() { if (g_hTuple != Empty_Handle) SQL_FreeHandle(g_hTuple) } /* ---------- ban lookup on connect ---------- */ public client_authorized(id) { if (g_hTuple == Empty_Handle || is_user_bot(id) || is_user_hltv(id)) return new authid[35], ip[24], safeAuth[64], safeIp[48] get_user_authid(id, authid, charsmax(authid)) get_user_ip(id, ip, charsmax(ip), 1) sqlEscape(authid, safeAuth, charsmax(safeAuth)) sqlEscape(ip, safeIp, charsmax(safeIp)) new query[512] formatex(query, charsmax(query), "SELECT `reason`, `expires`, `admin` FROM `%s` WHERE (`authid` = '%s' OR `ip` = '%s') AND (`expires` = 0 OR `expires` > %d) ORDER BY `id` DESC LIMIT 1", TABLE_NAME, safeAuth, safeIp, get_systime()) new data[2] data[0] = id data[1] = get_user_userid(id) SQL_ThreadQuery(g_hTuple, "handlerCheckBan", query, data, sizeof(data)) } public handlerCheckBan(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) { if (failstate != TQUERY_SUCCESS) { log_amx("[CSB Bans] ban lookup failed (%d): %s", errnum, error) return } if (!SQL_NumResults(query)) return new id = data[0] new userid = data[1] if (!is_user_connected(id) || get_user_userid(id) != userid) return new reason[128], admin[32], expires SQL_ReadResult(query, 0, reason, charsmax(reason)) expires = SQL_ReadResult(query, 1) SQL_ReadResult(query, 2, admin, charsmax(admin)) new left[32] if (expires == 0) copy(left, charsmax(left), "permanent") else formatex(left, charsmax(left), "%d minute(s) left", (expires - get_systime()) / 60) new kickmsg[64] get_pcvar_string(g_pKickMsg, kickmsg, charsmax(kickmsg)) new motd[512] formatex(motd, charsmax(motd), "

%s

Reason: %s
Banned by: %s
Duration: %s

Appeal on the server website.
", kickmsg, reason[0] ? reason : "no reason given", admin[0] ? admin : "CONSOLE", left) show_motd(id, motd, "Banned") log_amx("[CSB Bans] rejected banned client #%d (%s, %s)", userid, reason, left) server_cmd("kick #%d ^"%s^"", userid, kickmsg) } /* ---------- commands ---------- */ public cmdBan(id, level, cid) { if (!cmd_access(id, level, cid, 3)) return PLUGIN_HANDLED if (g_hTuple == Empty_Handle) { console_print(id, "[CSB] No database connection tuple; check the csb_ban_* cvars.") return PLUGIN_HANDLED } new arg[32], minsArg[8], reason[128] read_argv(1, arg, charsmax(arg)) read_argv(2, minsArg, charsmax(minsArg)) read_argv(3, reason, charsmax(reason)) new target = cmd_target(id, arg, CMDTARGET_OBEY_IMMUNITY) if (!target) return PLUGIN_HANDLED new mins = str_to_num(minsArg) if (mins < 0) mins = 0 if (!reason[0]) copy(reason, charsmax(reason), "no reason given") new tname[32], tauth[35], tip[24], aname[32], aauth[35] get_user_name(target, tname, charsmax(tname)) get_user_authid(target, tauth, charsmax(tauth)) get_user_ip(target, tip, charsmax(tip), 1) if (id) { get_user_name(id, aname, charsmax(aname)) get_user_authid(id, aauth, charsmax(aauth)) } else { copy(aname, charsmax(aname), "CONSOLE") copy(aauth, charsmax(aauth), "CONSOLE") } new sName[64], sAuth[64], sIp[48], sAdmin[64], sAdminAuth[64], sReason[192] sqlEscape(tname, sName, charsmax(sName)) sqlEscape(tauth, sAuth, charsmax(sAuth)) sqlEscape(tip, sIp, charsmax(sIp)) sqlEscape(aname, sAdmin, charsmax(sAdmin)) sqlEscape(aauth, sAdminAuth, charsmax(sAdminAuth)) sqlEscape(reason, sReason, charsmax(sReason)) new now = get_systime() new expires = mins > 0 ? (now + mins * 60) : 0 new query[768] formatex(query, charsmax(query), "INSERT INTO `%s` (`authid`, `ip`, `player`, `admin`, `admin_authid`, `reason`, `created`, `expires`) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', %d, %d)", TABLE_NAME, sAuth, sIp, sName, sAdmin, sAdminAuth, sReason, now, expires) SQL_ThreadQuery(g_hTuple, "handlerSimple", query) new buf[192] if (mins > 0) formatex(buf, charsmax(buf), "^x04[CSB]^x03 %s^x01 banned^x03 %s^x01 for^x04 %d^x01 minute(s) (%s)", aname, tname, mins, reason) else formatex(buf, charsmax(buf), "^x04[CSB]^x03 %s^x01 banned^x03 %s^x01 permanently (%s)", aname, tname, reason) chatAll(buf) log_amx("[CSB Bans] %s <%s> banned %s <%s / %s> for %d min: %s", aname, aauth, tname, tauth, tip, mins, reason) new kickmsg[64] get_pcvar_string(g_pKickMsg, kickmsg, charsmax(kickmsg)) server_cmd("kick #%d ^"%s: %s^"", get_user_userid(target), kickmsg, reason) return PLUGIN_HANDLED } public cmdUnban(id, level, cid) { if (!cmd_access(id, level, cid, 2)) return PLUGIN_HANDLED new authid[35], safe[64] read_argv(1, authid, charsmax(authid)) sqlEscape(authid, safe, charsmax(safe)) if (!safe[0]) { console_print(id, "[CSB] Usage: amx_unban ") return PLUGIN_HANDLED } new query[256] formatex(query, charsmax(query), "DELETE FROM `%s` WHERE `authid` = '%s'", TABLE_NAME, safe) SQL_ThreadQuery(g_hTuple, "handlerSimple", query) new aname[32] get_user_name(id, aname, charsmax(aname)) if (!id) copy(aname, charsmax(aname), "CONSOLE") console_print(id, "[CSB] Unban queued for %s.", safe) log_amx("[CSB Bans] %s unbanned %s", aname, safe) server_cmd("removeid %s", safe) server_cmd("writeid") return PLUGIN_HANDLED } public cmdBans(id, level, cid) { if (!cmd_access(id, level, cid, 1)) return PLUGIN_HANDLED new query[256] formatex(query, charsmax(query), "SELECT `player`, `authid`, `admin`, `reason`, `expires` FROM `%s` ORDER BY `id` DESC LIMIT 20", TABLE_NAME) new data[1] data[0] = id SQL_ThreadQuery(g_hTuple, "handlerBanList", query, data, sizeof(data)) return PLUGIN_HANDLED } public handlerBanList(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) { if (failstate != TQUERY_SUCCESS) { log_amx("[CSB Bans] ban list failed (%d): %s", errnum, error) return } new id = data[0] if (!is_user_connected(id)) return new motd[1536], len = 0 len += formatex(motd[len], charsmax(motd) - len, "") new player[32], authid[35], admin[32], reason[64], expires, when[32] new now = get_systime() while (SQL_MoreResults(query) && len < charsmax(motd) - 220) { SQL_ReadResult(query, 0, player, charsmax(player)) SQL_ReadResult(query, 1, authid, charsmax(authid)) SQL_ReadResult(query, 2, admin, charsmax(admin)) SQL_ReadResult(query, 3, reason, charsmax(reason)) expires = SQL_ReadResult(query, 4) if (expires == 0) copy(when, charsmax(when), "never") else if (expires <= now) copy(when, charsmax(when), "expired") else formatex(when, charsmax(when), "%d min", (expires - now) / 60) len += formatex(motd[len], charsmax(motd) - len, "", player, authid, admin, reason, when) SQL_NextRow(query) } len += formatex(motd[len], charsmax(motd) - len, "
PlayerSteamIDAdminReasonExpires
%s%s%s%s%s
") show_motd(id, motd, "CSB Bans") } public handlerSimple(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) { if (failstate != TQUERY_SUCCESS) log_amx("[CSB Bans] query failed (%d): %s", errnum, error) } /* ---------- helpers ---------- */ stock sqlEscape(const input[], output[], len) { new i = 0, j = 0, c while (input[i] != 0 && j < len) { c = input[i] /* strip quote, double quote, backslash, semicolon, percent and backtick */ if (c == 39 || c == 34 || c == 92 || c == 59 || c == 37 || c == 96 || c < 32) { i++ continue } output[j++] = c i++ } output[j] = 0 } stock chatAll(const msg[]) { new players[32], num, msgid = get_user_msgid("SayText") get_players(players, num, "ch") for (new i = 0; i < num; i++) { message_begin(MSG_ONE, msgid, _, players[i]) write_byte(players[i]) write_string(msg) message_end() } }