/* * 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") }