Promouvoir serveur
csb-admin-logger / addons / amxmodx / scripting / csb_admin_logger.sma
200 lines 5.8 KiB Pawn Raw Permalink History
/*
 * 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 <https://www.gnu.org/licenses/> for details.
 */

#include <amxmodx>
#include <amxmisc>
#include <sqlx>

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