/*
* CSB Admin Logger
* Copyright (C) 2026 counter-strike-boost.com
*
* Logs every admin command (kick, ban, gag, slay, map change and any action
* pushed through the csb_log_action native) with the admin's SteamID, name, the
* target/args, the current map and a timestamp into a MySQL table via threaded
* SQLx. If the database is unreachable it falls back to a flat log file.
*
* 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 for details.
*/
#include
#include
#include
new const PLUGIN[] = "CSB Admin Logger"
new const VERSION[] = "1.0.0"
new const AUTHOR[] = "counter-strike-boost.com"
#define FALLBACK_FILE "addons/amxmodx/logs/csb_admin_actions.log"
new const g_watched[][] = { "amx_kick", "amx_ban", "amx_banip", "amx_gag", "amx_slay", "amx_slap", "amx_map" }
new Handle:g_hTuple = Empty_Handle
new bool:g_bDbReady = false
new bool:g_bTableChecked = false
new g_pHost, g_pUser, g_pPass, g_pDb
public plugin_init()
{
register_plugin(PLUGIN, VERSION, AUTHOR)
g_pHost = register_cvar("csb_log_sql_host", "127.0.0.1")
g_pUser = register_cvar("csb_log_sql_user", "root")
g_pPass = register_cvar("csb_log_sql_pass", "")
g_pDb = register_cvar("csb_log_sql_db", "amxx")
for (new i = 0; i < sizeof(g_watched); i++)
register_clcmd(g_watched[i], "cmdWatch")
}
public plugin_natives()
{
register_native("csb_log_action", "native_log_action", 0)
}
public plugin_cfg()
{
new host[64], user[32], pass[32], db[32]
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))
if (!host[0] || !user[0] || !db[0])
{
log_amx("[CSB Logger] SQL not configured; using flat-file fallback only.")
return
}
g_hTuple = SQL_MakeDbTuple(host, user, pass, db)
g_bDbReady = true
new query[256]
formatex(query, charsmax(query),
"CREATE TABLE IF NOT EXISTS csb_admin_log (id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, \
stamp DATETIME, map VARCHAR(64), admin_name VARCHAR(64), admin_auth VARCHAR(48), \
target VARCHAR(64), action VARCHAR(255))")
SQL_ThreadQuery(g_hTuple, "queryCreate", query)
}
public plugin_end()
{
if (g_hTuple != Empty_Handle)
SQL_FreeHandle(g_hTuple)
}
public queryCreate(failstate, Handle:query, error[], errcode, data[], size, Float:queuetime)
{
if (failstate != TQUERY_SUCCESS)
{
log_amx("[CSB Logger] table check failed (%d): %s - falling back to file.", errcode, error)
g_bDbReady = false
return
}
g_bTableChecked = true
}
/* native: csb_log_action(const admin_auth[], const admin_name[], const target[], const action[]) */
public native_log_action(plugin, params)
{
new adminAuth[48], adminName[64], target[64], action[128]
get_string(1, adminAuth, charsmax(adminAuth))
get_string(2, adminName, charsmax(adminName))
get_string(3, target, charsmax(target))
get_string(4, action, charsmax(action))
storeAction(adminName, adminAuth, target, action)
return 1
}
public cmdWatch(id)
{
new cmd[32]
read_argv(0, cmd, charsmax(cmd))
new target[64]
read_argv(1, target, charsmax(target))
new args[128]
read_args(args, charsmax(args))
remove_quotes(args)
new adminName[64], adminAuth[48]
getName(id, adminName, charsmax(adminName))
if (id)
get_user_authid(id, adminAuth, charsmax(adminAuth))
else
copy(adminAuth, charsmax(adminAuth), "CONSOLE")
new action[160]
formatex(action, charsmax(action), "%s %s", cmd, args)
storeAction(adminName, adminAuth, target, action)
return PLUGIN_CONTINUE
}
storeAction(const adminName[], const adminAuth[], const target[], const action[])
{
new sName[64], sAuth[48], sTarget[64], sAction[160]
sanitize(sName, charsmax(sName), adminName)
sanitize(sAuth, charsmax(sAuth), adminAuth)
sanitize(sTarget, charsmax(sTarget), target)
sanitize(sAction, charsmax(sAction), action)
new map[64]
get_mapname(map, charsmax(map))
if (g_bDbReady && g_hTuple != Empty_Handle)
{
new query[512]
formatex(query, charsmax(query),
"INSERT INTO csb_admin_log (stamp, map, admin_name, admin_auth, target, action) \
VALUES (NOW(), '%s', '%s', '%s', '%s', '%s')",
map, sName, sAuth, sTarget, sAction)
SQL_ThreadQuery(g_hTuple, "queryInsert", query)
}
else
{
writeFallback(map, sName, sAuth, sTarget, sAction)
}
}
public queryInsert(failstate, Handle:query, error[], errcode, data[], size, Float:queuetime)
{
if (failstate != TQUERY_SUCCESS)
{
log_amx("[CSB Logger] insert failed (%d): %s", errcode, error)
g_bDbReady = false
}
}
writeFallback(const map[], const name[], const auth[], const target[], const action[])
{
new stamp[32]
get_time("%Y-%m-%d %H:%M:%S", stamp, charsmax(stamp))
new line[320]
formatex(line, charsmax(line), "[%s] map=%s admin=%s (%s) target=%s action=%s",
stamp, map, name, auth, target, action)
write_file(FALLBACK_FILE, line)
}
/* strip characters that would break the single-quoted SQL literal */
sanitize(out[], len, const in[])
{
copy(out, len, in)
replace_all(out, len, "'", " ")
replace_all(out, len, "\", " ")
replace_all(out, len, "^n", " ")
}
getName(id, name[], len)
{
if (id)
get_user_name(id, name, len)
else
copy(name, len, "CONSOLE")
}