/*
* 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, "| Player | SteamID | Admin | Reason | Expires |
")
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, "| %s | %s | %s | %s | %s |
", player, authid, admin, reason, when)
SQL_NextRow(query)
}
len += formatex(motd[len], charsmax(motd) - len, "
")
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()
}
}