diff --git a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs index 2c85a41..f14ed8d 100644 --- a/CS2-SimpleAdmin/CS2-SimpleAdmin.cs +++ b/CS2-SimpleAdmin/CS2-SimpleAdmin.cs @@ -22,7 +22,7 @@ public partial class CS2_SimpleAdmin : BasePlugin, IPluginConfig "CS2-SimpleAdmin" + (Helper.IsDebugBuild ? " (DEBUG)" : " (RELEASE)"); public override string ModuleDescription => "Simple admin plugin for Counter-Strike 2 :)"; public override string ModuleAuthor => "daffyy"; - public override string ModuleVersion => "1.8.2b-zio9"; + public override string ModuleVersion => "1.8.2b-zio10"; public override void Load(bool hotReload) { diff --git a/CS2-SimpleAdmin/Commands/basecommands.cs b/CS2-SimpleAdmin/Commands/basecommands.cs index 122ca85..e2a2b30 100644 --- a/CS2-SimpleAdmin/Commands/basecommands.cs +++ b/CS2-SimpleAdmin/Commands/basecommands.cs @@ -286,7 +286,9 @@ public partial class CS2_SimpleAdmin if (DatabaseProvider == null) return; var flagsList = flags.Split(',').Select(flag => flag.Trim()).ToList(); - _ = Instance.PermissionManager.AddAdminBySteamId(steamid, name, flagsList, immunity, time, globalAdmin); + // zio: who did it, for the rank log (null caller is Console, which includes RCON). + _ = Instance.PermissionManager.AddAdminBySteamId(steamid, name, flagsList, immunity, time, globalAdmin, + caller?.SteamID ?? 0, caller?.PlayerName ?? "Console"); Helper.LogCommand(caller, $"css_addadmin {steamid} {name} {flags} {immunity} {time}"); @@ -331,7 +333,8 @@ public partial class CS2_SimpleAdmin public void RemoveAdmin(CCSPlayerController? caller, string steamid, bool globalDelete = false, CommandInfo? command = null) { if (DatabaseProvider == null) return; - _ = PermissionManager.DeleteAdminBySteamId(steamid, globalDelete); + // zio: who did it, for the rank log (null caller is Console, which includes RCON). + _ = PermissionManager.DeleteAdminBySteamId(steamid, globalDelete, caller?.SteamID ?? 0, caller?.PlayerName ?? "Console"); AddTimer(2, () => { diff --git a/CS2-SimpleAdmin/Helper.cs b/CS2-SimpleAdmin/Helper.cs index 5851218..105be3b 100644 --- a/CS2-SimpleAdmin/Helper.cs +++ b/CS2-SimpleAdmin/Helper.cs @@ -158,21 +158,21 @@ internal static class Helper return check.CanExecuteCommand(player); } - // zio: rank groups, highest first, and their chat abbreviations. Guardian (VIP) is last, so a - // staff rank wins. + // zio: rank groups, highest first, and their chat tags (the same as CS2-Tags' chat prefixes). + // Guardian (VIP) is last, so a staff rank wins. private static readonly (string Group, string Tag)[] StaffRanks = [ ("#rank/owner", "O"), - ("#rank/senioradmin", "SA"), - ("#rank/admin", "A"), - ("#rank/trialadmin", "TA"), - ("#rank/mod", "M"), - ("#rank/trialmod", "TM"), - ("#rank/helper", "H"), - ("#rank/guardian", "G"), + ("#rank/senioradmin", "S.Admin"), + ("#rank/admin", "Admin"), + ("#rank/trialadmin", "T.Admin"), + ("#rank/mod", "Mod"), + ("#rank/trialmod", "T.Mod"), + ("#rank/helper", "Helper"), + ("#rank/guardian", "Guardian"), ]; - // zio: the player's highest rank abbreviation, or null if they're in no rank group. + // zio: the player's highest rank tag, or null if they're in no rank group. internal static string? GetStaffRankTag(CCSPlayerController player) { var groups = AdminManager.GetPlayerAdminData(new SteamID(player.SteamID))?.Groups; diff --git a/CS2-SimpleAdmin/Managers/PermissionManager.cs b/CS2-SimpleAdmin/Managers/PermissionManager.cs index 5826e74..73d191c 100644 --- a/CS2-SimpleAdmin/Managers/PermissionManager.cs +++ b/CS2-SimpleAdmin/Managers/PermissionManager.cs @@ -247,7 +247,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// /// The SteamID of the admin to delete. /// Whether to delete the admin globally or only for the current server. - public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false) + /// zio: SteamID64 of who removed them, 0 for Console, for the rank log. + /// zio: name of who removed them, for the rank log. + public async Task DeleteAdminBySteamId(string playerSteamId, bool globalDelete = false, ulong adminSteamId = 0, string adminName = "Console") { if (databaseProvider == null) return; if (string.IsNullOrEmpty(playerSteamId)) return; @@ -255,8 +257,14 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) try { await using var connection = await databaseProvider.CreateConnectionAsync(); + // zio: note the rank and name before they're gone, for the rank log. + var before = await GetRank(connection, playerSteamId); + var playerName = await connection.ExecuteScalarAsync( + "SELECT player_name FROM sa_admins WHERE player_steamid = @playerSteamId ORDER BY server_id IS NULL DESC, id LIMIT 1", + new { playerSteamId }) ?? ""; var sql = databaseProvider.GetDeleteAdminQuery(globalDelete); await connection.ExecuteAsync(sql, new { PlayerSteamID = playerSteamId, CS2_SimpleAdmin.ServerId }); + await LogRankChange(connection, playerSteamId, playerName, before, await GetRank(connection, playerSteamId), adminSteamId, adminName); } catch (Exception ex) { @@ -273,7 +281,9 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) /// Immunity level. /// Duration in minutes for admin expiration; 0 means permanent. /// Whether the admin is global or server-specific. - public async Task AddAdminBySteamId(string playerSteamId, string playerName, List flagsList, int immunity = 0, int time = 0, bool globalAdmin = false) + /// zio: SteamID64 of who added them, 0 for Console, for the rank log. + /// zio: name of who added them, for the rank log. + public async Task AddAdminBySteamId(string playerSteamId, string playerName, List flagsList, int immunity = 0, int time = 0, bool globalAdmin = false, ulong adminSteamId = 0, string adminName = "Console") { if (databaseProvider == null) return; @@ -285,6 +295,7 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) try { await using var connection = await databaseProvider.CreateConnectionAsync(); + var before = await GetRank(connection, playerSteamId); // zio: for the rank log var insertAdminSql = databaseProvider.GetAddAdminQuery(); var adminId = await connection.ExecuteScalarAsync(insertAdminSql, new @@ -307,6 +318,8 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) }); } + await LogRankChange(connection, playerSteamId, playerName, before, await GetRank(connection, playerSteamId), adminSteamId, adminName); + await Server.NextWorldUpdateAsync(() => { CS2_SimpleAdmin.Instance.ReloadAdmins(null); @@ -318,6 +331,69 @@ public class PermissionManager(IDatabaseProvider? databaseProvider) } } + // zio: rank history in sa_rank_log, a table owned by our web panel (simpleadmin-web's + // sql/rank-log.sql), which logs its own changes there too. A player's rank is their + // highest-immunity group; a change is only logged when that changes. + private sealed class RankRow + { + public string Name { get; set; } = ""; + public int Immunity { get; set; } + } + + private static bool _rankLogMissingWarned; + + // zio: the player's current rank, or null. Only unexpired admin rows and groups that apply to + // this server count, the same set SimpleAdmin loads. + private static async Task GetRank(System.Data.Common.DbConnection connection, string playerSteamId) + { + return await connection.QueryFirstOrDefaultAsync(""" + SELECT g.name AS Name, g.immunity AS Immunity + FROM sa_admins a + JOIN sa_admins_flags f ON f.admin_id = a.id + JOIN sa_groups g ON g.name = f.flag + JOIN sa_groups_servers gs ON gs.group_id = g.id AND (gs.server_id IS NULL OR gs.server_id = @serverid) + WHERE a.player_steamid = @playerSteamId AND (a.ends IS NULL OR a.ends > @now) + AND (a.server_id IS NULL OR a.server_id = @serverid) + ORDER BY g.immunity DESC, g.id LIMIT 1 + """, new { playerSteamId, now = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId }); + } + + // zio: records a rank change in sa_rank_log. Never fails the admin change itself: if the table + // is missing (the panel hasn't created it), it warns once and carries on. + private static async Task LogRankChange(System.Data.Common.DbConnection connection, string playerSteamId, string playerName, + RankRow? before, RankRow? after, ulong adminSteamId, string adminName) + { + if (before?.Name == after?.Name) return; + var kind = before == null ? "added" + : after == null ? "removed" + : after.Immunity > before.Immunity ? "promoted" + : after.Immunity < before.Immunity ? "demoted" + : "changed"; + try + { + await connection.ExecuteAsync(""" + INSERT INTO sa_rank_log (player_steamid, player_name, old_rank, new_rank, kind, admin_steamid, admin_name, source, created) + VALUES (@playerSteamId, @playerName, @oldRank, @newRank, @kind, @adminSteamId, @adminName, 'game', @created) + """, new + { + playerSteamId = ulong.Parse(playerSteamId), + playerName, + oldRank = before?.Name, + newRank = after?.Name, + kind, + adminSteamId, + adminName, + created = Time.ActualDateTime() + }); + } + catch (Exception ex) + { + if (_rankLogMissingWarned) return; + _rankLogMissingWarned = true; + CS2_SimpleAdmin._logger?.LogWarning($"Couldn't write sa_rank_log (the web panel creates it): {ex.Message}"); + } + } + /// /// Adds a new group with flags and immunity asynchronously. /// diff --git a/README.md b/README.md index a1bd016..4a2bdb0 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: - New permission `@css/bypassipban`, for players or groups: IP bans, and bans on other accounts that share their IP, don't apply to them; a ban on their own SteamID still does. `@css/root` has it, as it has every `@css` flag. -- Staff chat shows the sender's rank, from their highest `#rank/...` group (O, SA, A, TA, M, TM, - H for helper, G for guardian): `@message` in team chat and `css_asay` read +- Staff chat shows the sender's rank, from their highest `#rank/...` group (O, S.Admin, Admin, + T.Admin, Mod, T.Mod, Helper, Guardian, the same as CS2-Tags' chat prefixes): `@message` in team chat and `css_asay` read `>[STAFF][RANK] name: message`, and `@message` in all chat and `css_say` from a player read `[RANK] name: message` (`name: message` if they have no rank group). A non-staff player's `@message` in team chat reads @@ -29,6 +29,10 @@ from the `build-1.8.2b` tag). Changes, all marked `zio:` in the code: as CSS checks the typed command. Upstream required every flag in the override. - `css_penalties ` (someone else's penalties) needs `@zio/penalties` or `@css/root` instead of `@css/kick`. +- `css_addadmin`, `css_deladmin` and the admin menu's add/remove log rank changes (the player's + highest-immunity group, and who changed it) to `sa_rank_log`, the table simpleadmin-web creates + and lists as the public promotions/demotions log. If the table is missing, it warns once in the + log and the admin change goes ahead. Publish with `FORGEJO_TOKEN=... ./release.sh v` (ModuleVersion is `1.8.2b-zioN`). It rebuilds from the committed source and uploads `SimpleAdmin-.tar.gz` to git.zio.sh