mirror of
https://github.com/daffyyyy/CS2-SimpleAdmin.git
synced 2026-10-18 12:57:04 +02:00
upstream to 1.7.7-alpha-10
This commit is contained in:
commit
2de2924cef
106 changed files with 5897 additions and 3306 deletions
|
|
@ -5,36 +5,37 @@ using Dapper;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using MySqlConnector;
|
||||
using System.Text;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
internal class BanManager(Database.Database? database)
|
||||
internal class BanManager(IDatabaseProvider? databaseProvider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Bans an online player and inserts the ban record into the database.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to be banned (must be currently online).</param>
|
||||
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
|
||||
/// <param name="reason">The reason for the ban.</param>
|
||||
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
|
||||
/// <returns>The newly created ban ID if successful, otherwise null.</returns>
|
||||
public async Task<int?> BanPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (database == null) return null;
|
||||
|
||||
if (databaseProvider == null) return null;
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
await using MySqlConnection connection = await database.GetConnectionAsync();
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
try
|
||||
{
|
||||
const string sql = """
|
||||
|
||||
INSERT INTO `sa_bans`
|
||||
(`player_steamid`, `player_name`, `player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @playerName, @playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
|
||||
var sql = databaseProvider.GetAddBanQuery();
|
||||
|
||||
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = player.SteamId.SteamId64.ToString(),
|
||||
playerSteamid = player.SteamId.SteamId64,
|
||||
playerName = player.Name,
|
||||
playerIp = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 1 ? player.IpAddress : null,
|
||||
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
banReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -45,37 +46,36 @@ internal class BanManager(Database.Database? database)
|
|||
|
||||
return banId;
|
||||
}
|
||||
catch
|
||||
catch(Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int?> AddBanBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
|
||||
/// <summary>
|
||||
/// Adds a ban for an offline player identified by their SteamID.
|
||||
/// </summary>
|
||||
/// <param name="playerSteamId">The SteamID64 of the player to ban.</param>
|
||||
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
|
||||
/// <param name="reason">The reason for the ban.</param>
|
||||
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
|
||||
/// <returns>The ID of the newly created ban if successful, otherwise null.</returns>
|
||||
public async Task<int?> AddBanBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (database == null) return null;
|
||||
if (string.IsNullOrEmpty(playerSteamId)) return null;
|
||||
if (databaseProvider == null) return null;
|
||||
|
||||
DateTime now = Time.ActualDateTime();
|
||||
DateTime futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using MySqlConnection connection = await database.GetConnectionAsync();
|
||||
|
||||
const string sql = """
|
||||
|
||||
INSERT INTO `sa_bans`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddBanBySteamIdQuery();
|
||||
var banId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = playerSteamId,
|
||||
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
banReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -86,15 +86,23 @@ internal class BanManager(Database.Database? database)
|
|||
|
||||
return banId;
|
||||
}
|
||||
catch (Exception)
|
||||
catch(Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError(ex, ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a ban for an offline player identified by their IP address.
|
||||
/// </summary>
|
||||
/// <param name="playerIp">The IP address of the player to ban.</param>
|
||||
/// <param name="issuer">The admin issuing the ban. Can be null if issued from console.</param>
|
||||
/// <param name="reason">The reason for the ban.</param>
|
||||
/// <param name="time">Ban duration in minutes. If 0, the ban is permanent.</param>
|
||||
public async Task AddBanByIp(string playerIp, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
if (string.IsNullOrEmpty(playerIp)) return;
|
||||
|
||||
|
|
@ -103,15 +111,13 @@ internal class BanManager(Database.Database? database)
|
|||
|
||||
try
|
||||
{
|
||||
await using MySqlConnection connection = await database.GetConnectionAsync();
|
||||
|
||||
var sql = "INSERT INTO `sa_bans` (`player_ip`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`) " +
|
||||
"VALUES (@playerIp, @adminSteamid, @adminName, @banReason, @duration, @ends, @created, @serverid)";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetAddBanByIpQuery();
|
||||
await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
playerIp,
|
||||
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
banReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -123,370 +129,308 @@ internal class BanManager(Database.Database? database)
|
|||
catch { }
|
||||
}
|
||||
|
||||
public async Task<bool> IsPlayerBanned(PlayerInfo player)
|
||||
// public async Task<bool> IsPlayerBanned(PlayerInfo player)
|
||||
// {
|
||||
// if (database == null) return false;
|
||||
//
|
||||
// if (player.IpAddress == null)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// #if DEBUG
|
||||
// if (CS2_SimpleAdmin._logger != null)
|
||||
// CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
|
||||
// #endif
|
||||
//
|
||||
// int banCount;
|
||||
//
|
||||
// DateTime currentTime = Time.ActualDateTime();
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// string sql;
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
|
||||
// {
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
// SELECT COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// ), 0)
|
||||
// +
|
||||
// COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
// WHERE sa_bans.status = 'ACTIVE'
|
||||
// AND sa_players_ips.address = @PlayerIP
|
||||
// AND NOT EXISTS (
|
||||
// SELECT 1
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// )
|
||||
// ), 0) AS TotalBanCount;
|
||||
// """ : """
|
||||
// SELECT COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// AND server_id = @ServerId
|
||||
// ), 0)
|
||||
// +
|
||||
// COALESCE((
|
||||
// SELECT COUNT(*)
|
||||
// FROM sa_bans
|
||||
// JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
// WHERE sa_bans.status = 'ACTIVE'
|
||||
// AND sa_players_ips.address = @PlayerIP
|
||||
// AND NOT EXISTS (
|
||||
// SELECT 1
|
||||
// FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime)
|
||||
// AND server_id = @ServerId
|
||||
// )
|
||||
// ), 0) AS TotalBanCount;
|
||||
// """;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
// UPDATE sa_bans
|
||||
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime);
|
||||
//
|
||||
// SELECT COUNT(*) FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime);
|
||||
// """ : """
|
||||
// UPDATE sa_bans
|
||||
// SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
// player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
//
|
||||
// SELECT COUNT(*) FROM sa_bans
|
||||
// WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
// AND status = 'ACTIVE'
|
||||
// AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
// """;
|
||||
// }
|
||||
//
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
//
|
||||
// var parameters = new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
|
||||
// string.IsNullOrEmpty(player.IpAddress) ||
|
||||
// CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
|
||||
// ? null
|
||||
// : player.IpAddress,
|
||||
// PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
|
||||
// CurrentTime = currentTime,
|
||||
// CS2_SimpleAdmin.ServerId
|
||||
// };
|
||||
//
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
|
||||
// player.Name, ex.Message);
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return banCount > 0;
|
||||
// }
|
||||
//
|
||||
// public async Task<int> GetPlayerBans(PlayerInfo player)
|
||||
// {
|
||||
// if (database == null) return 0;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// string sql;
|
||||
//
|
||||
// sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
// ? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
|
||||
// : "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
|
||||
//
|
||||
// int banCount;
|
||||
//
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
|
||||
// {
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
// new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = player.IpAddress,
|
||||
// serverid = CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
// new
|
||||
// {
|
||||
// PlayerSteamID = player.SteamId.SteamId64,
|
||||
// PlayerIP = DBNull.Value,
|
||||
// serverid = CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return banCount;
|
||||
// }
|
||||
// catch { }
|
||||
//
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Unbans a player based on a pattern match of SteamID or IP address.
|
||||
/// </summary>
|
||||
/// <param name="playerPattern">Pattern to match against player identifiers (e.g., partial SteamID).</param>
|
||||
/// <param name="adminSteamId">SteamID64 of the admin performing the unban.</param>
|
||||
/// <param name="reason">Optional reason for the unban. If null or empty, the unban reason is not stored.</param>
|
||||
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
|
||||
{
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
if (playerPattern is not { Length: > 1 })
|
||||
{
|
||||
if (database == null) return false;
|
||||
|
||||
if (player.IpAddress == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (CS2_SimpleAdmin._logger != null)
|
||||
CS2_SimpleAdmin._logger.LogCritical($"IsPlayerBanned for {player.Name}");
|
||||
#endif
|
||||
|
||||
int banCount;
|
||||
|
||||
DateTime currentTime = Time.ActualDateTime();
|
||||
|
||||
try
|
||||
{
|
||||
string sql;
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp && !CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress))
|
||||
{
|
||||
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
SELECT COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM sa_bans
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
), 0)
|
||||
+
|
||||
COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM sa_bans
|
||||
JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
WHERE sa_bans.status = 'ACTIVE'
|
||||
AND sa_players_ips.address = @PlayerIP
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sa_bans
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
)
|
||||
), 0) AS TotalBanCount;
|
||||
""" : """
|
||||
SELECT COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM sa_bans
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
AND server_id = @ServerId
|
||||
), 0)
|
||||
+
|
||||
COALESCE((
|
||||
SELECT COUNT(*)
|
||||
FROM sa_bans
|
||||
JOIN sa_players_ips ON sa_bans.player_steamid = sa_players_ips.steamid
|
||||
WHERE sa_bans.status = 'ACTIVE'
|
||||
AND sa_players_ips.address = @PlayerIP
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sa_bans
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
AND server_id = @ServerId
|
||||
)
|
||||
), 0) AS TotalBanCount;
|
||||
""";
|
||||
}
|
||||
else
|
||||
{
|
||||
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
UPDATE sa_bans
|
||||
SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime);
|
||||
|
||||
SELECT COUNT(*) FROM sa_bans
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime);
|
||||
""" : """
|
||||
UPDATE sa_bans
|
||||
SET player_ip = CASE WHEN player_ip IS NULL THEN @PlayerIP ELSE player_ip END,
|
||||
player_name = CASE WHEN player_name IS NULL THEN @PlayerName ELSE player_name END
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
|
||||
SELECT COUNT(*) FROM sa_bans
|
||||
WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime) AND server_id = @ServerId;
|
||||
""";
|
||||
}
|
||||
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
|
||||
var parameters = new
|
||||
{
|
||||
PlayerSteamID = player.SteamId.SteamId64.ToString(),
|
||||
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0 ||
|
||||
string.IsNullOrEmpty(player.IpAddress) ||
|
||||
CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(player.IpAddress)
|
||||
? null
|
||||
: player.IpAddress,
|
||||
PlayerName = !string.IsNullOrEmpty(player.Name) ? player.Name : string.Empty,
|
||||
CurrentTime = currentTime,
|
||||
CS2_SimpleAdmin.ServerId
|
||||
};
|
||||
|
||||
banCount = await connection.ExecuteScalarAsync<int>(sql, parameters);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Unable to check ban status for {PlayerName} ({ExceptionMessage})",
|
||||
player.Name, ex.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return banCount > 0;
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task<int> GetPlayerBans(PlayerInfo player)
|
||||
try
|
||||
{
|
||||
if (database == null) return 0;
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sqlRetrieveBans = databaseProvider.GetUnbanRetrieveBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
try
|
||||
{
|
||||
string sql;
|
||||
|
||||
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)"
|
||||
: "SELECT COUNT(*) FROM sa_bans WHERE (player_steamid = @PlayerSteamID OR player_ip = @PlayerIP) AND server_id = @serverid";
|
||||
|
||||
int banCount;
|
||||
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0 && !string.IsNullOrEmpty(player.IpAddress))
|
||||
{
|
||||
banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
new
|
||||
{
|
||||
PlayerSteamID = player.SteamId.SteamId64.ToString(),
|
||||
PlayerIP = player.IpAddress,
|
||||
serverid = CS2_SimpleAdmin.ServerId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
banCount = await connection.ExecuteScalarAsync<int>(sql,
|
||||
new
|
||||
{
|
||||
PlayerSteamID = player.SteamId.SteamId64.ToString(),
|
||||
PlayerIP = DBNull.Value,
|
||||
serverid = CS2_SimpleAdmin.ServerId
|
||||
});
|
||||
}
|
||||
|
||||
return banCount;
|
||||
}
|
||||
catch { }
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task UnbanPlayer(string playerPattern, string adminSteamId, string reason)
|
||||
{
|
||||
if (database == null) return;
|
||||
|
||||
if (playerPattern is not { Length: > 1 })
|
||||
{
|
||||
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
|
||||
var bansList = bans as dynamic[] ?? bans.ToArray();
|
||||
if (bansList.Length == 0)
|
||||
return;
|
||||
}
|
||||
try
|
||||
|
||||
var sqlAdminId = databaseProvider.GetUnbanAdminIdQuery();
|
||||
var adminId = await connection.ExecuteScalarAsync<int?>(sqlAdminId, new { adminSteamId }) ?? 0;
|
||||
|
||||
foreach (var ban in bansList)
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
int banId = ban.id;
|
||||
|
||||
var sqlRetrieveBans = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE'"
|
||||
: "SELECT id FROM sa_bans WHERE (player_steamid = @pattern OR player_name = @pattern OR player_ip = @pattern) AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
var sqlInsertUnban = databaseProvider.GetInsertUnbanQuery(reason != null);
|
||||
var unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
|
||||
|
||||
var bans = await connection.QueryAsync(sqlRetrieveBans, new { pattern = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
|
||||
|
||||
var bansList = bans as dynamic[] ?? bans.ToArray();
|
||||
if (bansList.Length == 0)
|
||||
return;
|
||||
|
||||
const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
||||
var sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id, reason) VALUES (@banId, @adminId, @reason); SELECT LAST_INSERT_ID();";
|
||||
|
||||
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
|
||||
var adminId = sqlAdminId ?? 0;
|
||||
|
||||
foreach (var ban in bansList)
|
||||
{
|
||||
int banId = ban.id;
|
||||
int? unbanId;
|
||||
|
||||
if (reason != null)
|
||||
{
|
||||
unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId, reason });
|
||||
}
|
||||
else
|
||||
{
|
||||
sqlInsertUnban = "INSERT INTO sa_unbans (ban_id, admin_id) VALUES (@banId, @adminId); SELECT LAST_INSERT_ID();";
|
||||
unbanId = await connection.ExecuteScalarAsync<int>(sqlInsertUnban, new { banId, adminId });
|
||||
}
|
||||
|
||||
const string sqlUpdateBan = "UPDATE sa_bans SET status = 'UNBANNED', unban_id = @unbanId WHERE id = @banId";
|
||||
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
|
||||
}
|
||||
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
|
||||
{
|
||||
if (database == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0;
|
||||
|
||||
var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList();
|
||||
|
||||
var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList();
|
||||
var ipAddresses = filteredPlayers
|
||||
.Where(p => !string.IsNullOrEmpty(p.IpAddress))
|
||||
.Select(p => p.IpAddress)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var sql = new StringBuilder();
|
||||
sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' ");
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
sql.Append("AND (player_steamid IN @SteamIDs ");
|
||||
if (checkIpBans && ipAddresses.Count != 0)
|
||||
{
|
||||
sql.Append("OR player_ip IN @IpAddresses");
|
||||
}
|
||||
sql.Append(')');
|
||||
}
|
||||
else
|
||||
{
|
||||
sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs ");
|
||||
if (checkIpBans && ipAddresses.Count != 0)
|
||||
{
|
||||
sql.Append("OR player_ip IN @IpAddresses");
|
||||
}
|
||||
sql.Append(')');
|
||||
}
|
||||
|
||||
var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>(
|
||||
sql.ToString(),
|
||||
new
|
||||
{
|
||||
SteamIDs = steamIds,
|
||||
IpAddresses = checkIpBans ? ipAddresses : [],
|
||||
CS2_SimpleAdmin.ServerId
|
||||
});
|
||||
|
||||
var valueTuples = bannedPlayers.ToList();
|
||||
var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet();
|
||||
var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet();
|
||||
|
||||
foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) ||
|
||||
(checkIpBans && bannedIps.Contains(player.IpAddress ?? ""))))
|
||||
{
|
||||
if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.UserId.Value].WaitingForKick) continue;
|
||||
|
||||
await Server.NextFrameAsync(() =>
|
||||
{
|
||||
Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
|
||||
var sqlUpdateBan = databaseProvider.GetUpdateBanStatusQuery();
|
||||
await connection.ExecuteAsync(sqlUpdateBan, new { unbanId, banId });
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// public async Task CheckOnlinePlayers(List<(string? IpAddress, ulong SteamID, int? UserId, int Slot)> players)
|
||||
// {
|
||||
// if (database == null) return;
|
||||
//
|
||||
// try
|
||||
// {
|
||||
// await using var connection = await database.GetConnectionAsync();
|
||||
// bool checkIpBans = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType > 0;
|
||||
//
|
||||
// var filteredPlayers = players.Where(p => p.UserId.HasValue).ToList();
|
||||
//
|
||||
// var steamIds = filteredPlayers.Select(p => p.SteamID).Distinct().ToList();
|
||||
// var ipAddresses = filteredPlayers
|
||||
// .Where(p => !string.IsNullOrEmpty(p.IpAddress))
|
||||
// .Select(p => p.IpAddress)
|
||||
// .Distinct()
|
||||
// .ToList();
|
||||
//
|
||||
// var sql = new StringBuilder();
|
||||
// sql.Append("SELECT `player_steamid`, `player_ip` FROM `sa_bans` WHERE `status` = 'ACTIVE' ");
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
// {
|
||||
// sql.Append("AND (player_steamid IN @SteamIDs ");
|
||||
// if (checkIpBans && ipAddresses.Count != 0)
|
||||
// {
|
||||
// sql.Append("OR player_ip IN @IpAddresses");
|
||||
// }
|
||||
// sql.Append(')');
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// sql.Append("AND server_id = @ServerId AND (player_steamid IN @SteamIDs ");
|
||||
// if (checkIpBans && ipAddresses.Count != 0)
|
||||
// {
|
||||
// sql.Append("OR player_ip IN @IpAddresses");
|
||||
// }
|
||||
// sql.Append(')');
|
||||
// }
|
||||
//
|
||||
// var bannedPlayers = await connection.QueryAsync<(ulong PlayerSteamID, string PlayerIP)>(
|
||||
// sql.ToString(),
|
||||
// new
|
||||
// {
|
||||
// SteamIDs = steamIds,
|
||||
// IpAddresses = checkIpBans ? ipAddresses : [],
|
||||
// CS2_SimpleAdmin.ServerId
|
||||
// });
|
||||
//
|
||||
// var valueTuples = bannedPlayers.ToList();
|
||||
// var bannedSteamIds = valueTuples.Select(b => b.PlayerSteamID).ToHashSet();
|
||||
// var bannedIps = valueTuples.Select(b => b.PlayerIP).ToHashSet();
|
||||
//
|
||||
// foreach (var player in filteredPlayers.Where(player => bannedSteamIds.Contains(player.SteamID) ||
|
||||
// (checkIpBans && bannedIps.Contains(player.IpAddress ?? ""))))
|
||||
// {
|
||||
// if (!player.UserId.HasValue || CS2_SimpleAdmin.PlayersInfo[player.SteamID].WaitingForKick) continue;
|
||||
//
|
||||
// await Server.NextWorldUpdateAsync(() =>
|
||||
// {
|
||||
// Helper.KickPlayer(player.UserId.Value, NetworkDisconnectionReason.NETWORK_DISCONNECT_KICKBANADDED);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// CS2_SimpleAdmin._logger?.LogError($"Error checking online players: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Expires all bans that have passed their end time, including optional cleanup of old IP bans.
|
||||
/// </summary>
|
||||
public async Task ExpireOldBans()
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
var currentTime = Time.ActualDateTime();
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
/*
|
||||
string sql = "";
|
||||
await using MySqlConnection connection = await _database.GetConnectionAsync();
|
||||
|
||||
sql = "UPDATE sa_bans SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime";
|
||||
await connection.ExecuteAsync(sql, new { CurrentTime = DateTime.UtcNow });
|
||||
*/
|
||||
|
||||
string sql;
|
||||
|
||||
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
status = 'EXPIRED'
|
||||
WHERE
|
||||
status = 'ACTIVE'
|
||||
AND
|
||||
`duration` > 0
|
||||
AND
|
||||
ends <= @currentTime
|
||||
""" : """
|
||||
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
status = 'EXPIRED'
|
||||
WHERE
|
||||
status = 'ACTIVE'
|
||||
AND
|
||||
`duration` > 0
|
||||
AND
|
||||
ends <= @currentTime
|
||||
AND server_id = @serverid
|
||||
""";
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetExpireBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { currentTime, serverid = CS2_SimpleAdmin.ServerId });
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans > 0)
|
||||
{
|
||||
var ipBansTime = currentTime.AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
|
||||
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode ? """
|
||||
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = NULL
|
||||
WHERE
|
||||
status = 'ACTIVE'
|
||||
AND
|
||||
ends <= @ipBansTime
|
||||
""" : """
|
||||
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = NULL
|
||||
WHERE
|
||||
status = 'ACTIVE'
|
||||
AND
|
||||
ends <= @ipBansTime
|
||||
AND server_id = @serverid
|
||||
""";
|
||||
|
||||
sql = databaseProvider.GetExpireIpBansQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { ipBansTime, CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Concurrent;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using CS2_SimpleAdmin.Models;
|
||||
using Dapper;
|
||||
using ZLinq;
|
||||
|
|
@ -8,7 +9,7 @@ namespace CS2_SimpleAdmin.Managers;
|
|||
internal class CacheManager: IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, BanRecord> _banCache = [];
|
||||
private readonly ConcurrentDictionary<string, List<BanRecord>> _steamIdIndex = [];
|
||||
private readonly ConcurrentDictionary<ulong, List<BanRecord>> _steamIdIndex = [];
|
||||
private readonly ConcurrentDictionary<uint, List<BanRecord>> _ipIndex = [];
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, HashSet<IpRecord>> _playerIpsCache = [];
|
||||
|
|
@ -17,21 +18,26 @@ internal class CacheManager: IDisposable
|
|||
private DateTime _lastUpdateTime = DateTime.MinValue;
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and builds the ban and IP cache from the database. Loads bans, player IP history, and config settings.
|
||||
/// </summary>
|
||||
/// <returns>Asynchronous task representing the initialization process.</returns>
|
||||
public async Task InitializeCacheAsync()
|
||||
{
|
||||
if (CS2_SimpleAdmin.Database == null) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (!CS2_SimpleAdmin.ServerLoaded) return;
|
||||
if (_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
Clear();
|
||||
_cachedIgnoredIps = new HashSet<uint>(
|
||||
CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps
|
||||
.Select(IpHelper.IpToUint));
|
||||
_cachedIgnoredIps = CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps
|
||||
.AsValueEnumerable()
|
||||
.Select(IpHelper.IpToUint)
|
||||
.ToHashSet();
|
||||
|
||||
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
List<BanRecord> bans;
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
|
|
@ -40,6 +46,7 @@ internal class CacheManager: IDisposable
|
|||
"""
|
||||
SELECT
|
||||
id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
|
|
@ -52,6 +59,7 @@ internal class CacheManager: IDisposable
|
|||
"""
|
||||
SELECT
|
||||
id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
|
|
@ -59,20 +67,16 @@ internal class CacheManager: IDisposable
|
|||
WHERE server_id = @serverId
|
||||
""", new {serverId = CS2_SimpleAdmin.ServerId})).ToList();
|
||||
}
|
||||
|
||||
var ipHistory =
|
||||
await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC");
|
||||
|
||||
foreach (var ban in bans)
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
|
||||
{
|
||||
_banCache.TryAdd(ban.Id, ban);
|
||||
}
|
||||
|
||||
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
|
||||
{
|
||||
var ipSet = new HashSet<IpRecord>(
|
||||
group
|
||||
var ipHistory =
|
||||
(await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips ORDER BY used_at DESC")).ToList();
|
||||
|
||||
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
|
||||
{
|
||||
var ipSet = group
|
||||
.GroupBy(x => x.address)
|
||||
.Select(g =>
|
||||
{
|
||||
|
|
@ -80,32 +84,34 @@ internal class CacheManager: IDisposable
|
|||
return new IpRecord(
|
||||
g.Key,
|
||||
latest.used_at,
|
||||
!string.IsNullOrEmpty(latest.name)
|
||||
? latest.name
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
string.IsNullOrEmpty(latest.name)
|
||||
? CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
: latest.name
|
||||
);
|
||||
}),
|
||||
new IpRecordComparer()
|
||||
);
|
||||
})
|
||||
.ToHashSet(new IpRecordComparer());
|
||||
|
||||
_playerIpsCache.AddOrUpdate(
|
||||
group.Key,
|
||||
_ => ipSet,
|
||||
(_, existingSet) =>
|
||||
{
|
||||
foreach (var ip in ipSet)
|
||||
_playerIpsCache.AddOrUpdate(
|
||||
group.Key,
|
||||
_ => ipSet,
|
||||
(_, existingSet) =>
|
||||
{
|
||||
existingSet.Remove(ip);
|
||||
existingSet.Add(ip);
|
||||
}
|
||||
|
||||
return existingSet;
|
||||
});
|
||||
foreach (var ip in ipSet)
|
||||
{
|
||||
existingSet.Remove(ip);
|
||||
existingSet.Add(ip);
|
||||
}
|
||||
return existingSet;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var ban in bans.AsValueEnumerable())
|
||||
_banCache.TryAdd(ban.Id, ban);
|
||||
|
||||
RebuildIndexes();
|
||||
|
||||
_lastUpdateTime = DateTime.Now.AddSeconds(-1);
|
||||
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
|
||||
_isInitialized = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -114,27 +120,36 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached data and reinitializes the cache from the database.
|
||||
/// </summary>
|
||||
/// <returns>Asynchronous task representing the reinitialization process.</returns>
|
||||
public async Task ForceReInitializeCacheAsync()
|
||||
{
|
||||
_isInitialized = false;
|
||||
|
||||
_banCache.Clear();
|
||||
_playerIpsCache.Clear();
|
||||
_cachedIgnoredIps.Clear();
|
||||
_cachedIgnoredIps = [];
|
||||
_lastUpdateTime = DateTime.MinValue;
|
||||
|
||||
await InitializeCacheAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the in-memory cache with updated or new data from the database since the last update time.
|
||||
/// Also updates multi-account IP history if enabled.
|
||||
/// </summary>
|
||||
/// <returns>Asynchronous task representing the refresh operation.</returns>
|
||||
public async Task RefreshCacheAsync()
|
||||
{
|
||||
if (CS2_SimpleAdmin.Database == null) return;
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
if (!_isInitialized) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
|
||||
List<BanRecord> updatedBans;
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
IEnumerable<BanRecord> updatedBans;
|
||||
|
||||
var allIds = (await connection.QueryAsync<int>("SELECT id FROM sa_bans")).ToHashSet();
|
||||
|
||||
|
|
@ -143,45 +158,45 @@ internal class CacheManager: IDisposable
|
|||
updatedBans = (await connection.QueryAsync<BanRecord>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM `sa_bans` WHERE updated_at > @lastUpdate OR created > @lastUpdate ORDER BY updated_at DESC
|
||||
""",
|
||||
new { lastUpdate = _lastUpdateTime }
|
||||
)).ToList();
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
updatedBans = (await connection.QueryAsync<BanRecord>(
|
||||
"""
|
||||
SELECT id AS Id,
|
||||
player_name AS PlayerName,
|
||||
player_steamid AS PlayerSteamId,
|
||||
player_ip AS PlayerIp,
|
||||
status AS Status
|
||||
FROM `sa_bans` WHERE (updated_at > @lastUpdate OR created > @lastUpdate) AND server_id = @serverId ORDER BY updated_at DESC
|
||||
""",
|
||||
new { lastUpdate = _lastUpdateTime, serverId = CS2_SimpleAdmin.ServerId }
|
||||
)).ToList();
|
||||
));
|
||||
}
|
||||
|
||||
foreach (var id in _banCache.Keys)
|
||||
{
|
||||
if (allIds.Contains(id) || !_banCache.TryRemove(id, out var ban)) continue;
|
||||
|
||||
// Remove from steamIdIndex
|
||||
if (!string.IsNullOrWhiteSpace(ban.PlayerSteamId) &&
|
||||
_steamIdIndex.TryGetValue(ban.PlayerSteamId, out var steamBans))
|
||||
if (ban.PlayerSteamId != null &&
|
||||
_steamIdIndex.TryGetValue(ban.PlayerSteamId.Value, out var steamBans))
|
||||
{
|
||||
steamBans.RemoveAll(b => b.Id == id);
|
||||
if (steamBans.Count == 0)
|
||||
_steamIdIndex.TryRemove(ban.PlayerSteamId, out _);
|
||||
_steamIdIndex.TryRemove(ban.PlayerSteamId.Value, out _);
|
||||
}
|
||||
|
||||
// Remove from ipIndex
|
||||
if (!string.IsNullOrWhiteSpace(ban.PlayerIp) &&
|
||||
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) &&
|
||||
_ipIndex.TryGetValue(ipUInt, out var ipBans))
|
||||
if (string.IsNullOrWhiteSpace(ban.PlayerIp) ||
|
||||
!IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt) ||
|
||||
!_ipIndex.TryGetValue(ipUInt, out var ipBans)) continue;
|
||||
{
|
||||
ipBans.RemoveAll(b => b.Id == id);
|
||||
if (ipBans.Count == 0)
|
||||
|
|
@ -189,59 +204,64 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300", new {lastUpdate = _lastUpdateTime})).ToList();
|
||||
|
||||
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp)
|
||||
{
|
||||
var ipSet = new HashSet<IpRecord>(
|
||||
group
|
||||
.GroupBy(x => x.address)
|
||||
.Select(g =>
|
||||
{
|
||||
var latest = g.MaxBy(x => x.used_at);
|
||||
return new IpRecord(
|
||||
g.Key,
|
||||
latest.used_at,
|
||||
!string.IsNullOrEmpty(latest.name)
|
||||
? latest.name
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
);
|
||||
}),
|
||||
new IpRecordComparer()
|
||||
);
|
||||
var ipHistory = (await connection.QueryAsync<(ulong steamid, string? name, uint address, DateTime used_at)>(
|
||||
"SELECT steamid, name, address, used_at FROM sa_players_ips WHERE used_at >= @lastUpdate ORDER BY used_at DESC LIMIT 300",
|
||||
new { lastUpdate = _lastUpdateTime }));
|
||||
|
||||
_playerIpsCache.AddOrUpdate(
|
||||
group.Key,
|
||||
_ => ipSet,
|
||||
(_, existingSet) =>
|
||||
{
|
||||
foreach (var newEntry in ipSet)
|
||||
foreach (var group in ipHistory.AsValueEnumerable().GroupBy(x => x.steamid))
|
||||
{
|
||||
var ipSet = new HashSet<IpRecord>(
|
||||
group
|
||||
.GroupBy(x => x.address)
|
||||
.Select(g =>
|
||||
{
|
||||
var latest = g.MaxBy(x => x.used_at);
|
||||
return new IpRecord(
|
||||
g.Key,
|
||||
latest.used_at,
|
||||
!string.IsNullOrEmpty(latest.name)
|
||||
? latest.name
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
);
|
||||
}),
|
||||
new IpRecordComparer()
|
||||
);
|
||||
|
||||
_playerIpsCache.AddOrUpdate(
|
||||
group.Key,
|
||||
_ => ipSet,
|
||||
(_, existingSet) =>
|
||||
{
|
||||
existingSet.Remove(newEntry);
|
||||
existingSet.Add(newEntry);
|
||||
}
|
||||
return existingSet;
|
||||
});
|
||||
foreach (var newEntry in ipSet)
|
||||
{
|
||||
existingSet.Remove(newEntry);
|
||||
existingSet.Add(newEntry);
|
||||
}
|
||||
|
||||
return existingSet;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedBans.Count == 0)
|
||||
return;
|
||||
|
||||
|
||||
foreach (var ban in updatedBans)
|
||||
{
|
||||
_banCache.AddOrUpdate(ban.Id, ban, (_, _) => ban);
|
||||
}
|
||||
|
||||
RebuildIndexes();
|
||||
_lastUpdateTime = DateTime.Now.AddSeconds(-1);
|
||||
_lastUpdateTime = Time.ActualDateTime().AddSeconds(-1);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the internal indexes for fast lookup of active bans by Steam ID and IP address.
|
||||
/// Clears and repopulates both indexes based on the current in-memory ban cache.
|
||||
/// </summary>
|
||||
private void RebuildIndexes()
|
||||
{
|
||||
_steamIdIndex.Clear();
|
||||
|
|
@ -249,14 +269,14 @@ internal class CacheManager: IDisposable
|
|||
|
||||
foreach (var ban in _banCache.Values)
|
||||
{
|
||||
if (ban.Status != "ACTIVE")
|
||||
if (ban.StatusEnum != BanStatus.ACTIVE)
|
||||
continue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ban.PlayerSteamId))
|
||||
if (ban.PlayerSteamId != null)
|
||||
{
|
||||
var steamId = ban.PlayerSteamId;
|
||||
_steamIdIndex.AddOrUpdate(
|
||||
steamId,
|
||||
steamId.Value,
|
||||
key => [ban],
|
||||
(key, list) =>
|
||||
{
|
||||
|
|
@ -264,7 +284,9 @@ internal class CacheManager: IDisposable
|
|||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) continue;
|
||||
|
||||
if (ban.PlayerIp != null &&
|
||||
IpHelper.TryConvertIpToUint(ban.PlayerIp, out var ipUInt))
|
||||
{
|
||||
|
|
@ -280,71 +302,233 @@ internal class CacheManager: IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all ban records currently stored in the cache.
|
||||
/// </summary>
|
||||
/// <returns>List of all <see cref="BanRecord"/> objects.</returns>
|
||||
public List<BanRecord> GetAllBans() => _banCache.Values.ToList();
|
||||
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.Status == "ACTIVE").ToList();
|
||||
public List<BanRecord> GetPlayerBansBySteamId(string steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : [];
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves only active ban records from the cache.
|
||||
/// </summary>
|
||||
/// <returns>List of active <see cref="BanRecord"/> objects.</returns>
|
||||
public List<BanRecord> GetActiveBans() => _banCache.Values.Where(b => b.StatusEnum == BanStatus.ACTIVE).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all ban records for a specific player by their Steam ID.
|
||||
/// </summary>
|
||||
/// <param name="steamId">64-bit Steam ID of the player.</param>
|
||||
/// <returns>List of <see cref="BanRecord"/> objects associated with the Steam ID.</returns>
|
||||
public List<BanRecord> GetPlayerBansBySteamId(ulong steamId) => _steamIdIndex.TryGetValue(steamId, out var bans) ? bans : [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets all known Steam accounts that have used the specified IP address.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The IP address to search for, in string format.</param>
|
||||
/// <returns>
|
||||
/// List of tuples containing the Steam ID, last used time, and player name for each matching entry.
|
||||
/// </returns>
|
||||
public List<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
|
||||
{
|
||||
var ipAsUint = IpHelper.IpToUint(ipAddress);
|
||||
var results = new List<(ulong, DateTime, string)>();
|
||||
var comparer = _playerIpsCache.Comparer;
|
||||
|
||||
return _playerIpsCache.AsValueEnumerable()
|
||||
.SelectMany(kvp => kvp.Value
|
||||
.Where(entry => entry.Ip == ipAsUint)
|
||||
.Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName)))
|
||||
.ToList();
|
||||
foreach (var (steamId, ipSet) in _playerIpsCache)
|
||||
{
|
||||
if (!ipSet.TryGetValue(new IpRecord(ipAsUint, Time.ActualDateTime(), "Unknown"), out var actualEntry)) continue;
|
||||
results.Add((steamId, actualEntry.UsedAt, actualEntry.PlayerName));
|
||||
|
||||
foreach (var entry in ipSet)
|
||||
{
|
||||
if (entry.Ip == ipAsUint && !Equals(entry, actualEntry))
|
||||
{
|
||||
results.Add((steamId, entry.UsedAt, entry.PlayerName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// public IEnumerable<(ulong SteamId, DateTime UsedAt, string PlayerName)> GetAccountsByIp(string ipAddress)
|
||||
// {
|
||||
// var ipAsUint = IpHelper.IpToUint(ipAddress);
|
||||
//
|
||||
// return _playerIpsCache.SelectMany(kvp => kvp.Value
|
||||
// .Where(entry => entry.Ip == ipAsUint)
|
||||
// .Select(entry => (kvp.Key, entry.UsedAt, entry.PlayerName)));
|
||||
// }
|
||||
|
||||
private bool IsIpBanned(string ipAddress)
|
||||
{
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
|
||||
var ipUInt = IpHelper.IpToUint(ipAddress);
|
||||
return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
|
||||
}
|
||||
|
||||
|
||||
public bool IsPlayerBanned(string? steamId, string? ipAddress)
|
||||
// public bool IsPlayerBanned(ulong? steamId, string? ipAddress)
|
||||
// {
|
||||
// if (steamId != null && _steamIdIndex.ContainsKey(steamId.Value))
|
||||
// return true;
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
|
||||
// return false;
|
||||
//
|
||||
// if (string.IsNullOrEmpty(ipAddress) || !IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
|
||||
// return false;
|
||||
//
|
||||
// return !_cachedIgnoredIps.Contains(ipUInt) && _ipIndex.ContainsKey(ipUInt);
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player is currently banned by Steam ID or IP address.
|
||||
/// If a partial ban record is found, updates it with the latest player information.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Name of the player attempting to connect.</param>
|
||||
/// <param name="steamId">Optional 64-bit Steam ID of the player.</param>
|
||||
/// <param name="ipAddress">Optional IP address of the player.</param>
|
||||
/// <returns>True if the player is banned, otherwise false.</returns>
|
||||
public bool IsPlayerBanned(string playerName, ulong? steamId, string? ipAddress)
|
||||
{
|
||||
if (steamId != null && _steamIdIndex.ContainsKey(steamId))
|
||||
return true;
|
||||
BanRecord? record;
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
|
||||
{
|
||||
record = steamRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (record != null)
|
||||
{
|
||||
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
|
||||
(!record.PlayerSteamId.HasValue))
|
||||
{
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
|
||||
|
||||
if (ipAddress == null)
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
|
||||
return false;
|
||||
|
||||
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt))
|
||||
return false;
|
||||
|
||||
return !_cachedIgnoredIps.Contains(ipUInt) &&
|
||||
_ipIndex.ContainsKey(ipUInt);
|
||||
if (string.IsNullOrEmpty(ipAddress) ||
|
||||
!IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt) ||
|
||||
_cachedIgnoredIps.Contains(ipUInt) ||
|
||||
!_ipIndex.TryGetValue(ipUInt, out var ipRecords)) return false;
|
||||
|
||||
record = ipRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (record == null) return false;
|
||||
if ((string.IsNullOrEmpty(record.PlayerIp) && !string.IsNullOrEmpty(ipAddress)) ||
|
||||
(!record.PlayerSteamId.HasValue && steamId.HasValue))
|
||||
{
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
|
||||
// public bool IsPlayerOrAnyIpBanned(ulong steamId, string? ipAddress)
|
||||
// {
|
||||
// if (_steamIdIndex.ContainsKey(steamId))
|
||||
// return true;
|
||||
//
|
||||
// if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
|
||||
// return false;
|
||||
//
|
||||
// if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
|
||||
// return false;
|
||||
//
|
||||
// // var now = Time.ActualDateTime();
|
||||
// var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
|
||||
// var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
//
|
||||
// if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint))
|
||||
// {
|
||||
// if (!_cachedIgnoredIps.Contains(ipAsUint))
|
||||
// {
|
||||
// ipData.Add(new IpRecord(
|
||||
// ipAsUint,
|
||||
// Time.ActualDateTime().AddSeconds(-2),
|
||||
// unknownName
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // foreach (var ipRecord in ipData)
|
||||
// // {
|
||||
// // // Skip if too old or in ignored list
|
||||
// // if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
|
||||
// // continue;
|
||||
// //
|
||||
// // // Check if IP is banned
|
||||
// // if (_ipIndex.ContainsKey(ipRecord.Ip))
|
||||
// // return true;
|
||||
// // }
|
||||
//
|
||||
// foreach (var ipRecord in ipData)
|
||||
// {
|
||||
// if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
|
||||
// continue;
|
||||
//
|
||||
// if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords)) continue;
|
||||
//
|
||||
// var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
// if (activeBan == null) continue;
|
||||
//
|
||||
// if (!string.IsNullOrEmpty(activeBan.PlayerName) && activeBan.PlayerSteamId.HasValue) return true;
|
||||
//
|
||||
// _ = Task.Run(() => UpdatePlayerData(
|
||||
// activeBan.PlayerName,
|
||||
// steamId,
|
||||
// ipAddress
|
||||
// ));
|
||||
//
|
||||
// if (string.IsNullOrEmpty(activeBan.PlayerName) && !string.IsNullOrEmpty(unknownName))
|
||||
// activeBan.PlayerName = unknownName;
|
||||
//
|
||||
// activeBan.PlayerSteamId ??= steamId;
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player or any IP previously associated with them is currently banned.
|
||||
/// Also updates ban records with missing player info if found.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Current player name.</param>
|
||||
/// <param name="steamId">64-bit Steam ID of the player.</param>
|
||||
/// <param name="ipAddress">Current IP address of the player (optional).</param>
|
||||
/// <returns>True if the player or their known IPs are banned, otherwise false.</returns>
|
||||
public bool IsPlayerOrAnyIpBanned(string playerName, ulong steamId, string? ipAddress)
|
||||
{
|
||||
var steamIdStr = steamId.ToString();
|
||||
|
||||
if (_steamIdIndex.ContainsKey(steamIdStr))
|
||||
return true;
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0) return false;
|
||||
if (_steamIdIndex.TryGetValue(steamId, out var steamBans))
|
||||
{
|
||||
var activeBan = steamBans.FirstOrDefault(b => b.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(activeBan.PlayerName) || string.IsNullOrEmpty(activeBan.PlayerIp))
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0)
|
||||
return false;
|
||||
|
||||
if (!_playerIpsCache.TryGetValue(steamId, out var ipData))
|
||||
return false;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var cutoff = now.AddDays(-7);
|
||||
var cutoff = Time.ActualDateTime().AddDays(-CS2_SimpleAdmin.Instance.Config.OtherSettings.ExpireOldIpBans);
|
||||
var unknownName = CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
|
||||
if (ipAddress != null)
|
||||
if (ipAddress != null && IpHelper.TryConvertIpToUint(ipAddress, out var ipAsUint))
|
||||
{
|
||||
var ipAsUint = IpHelper.IpToUint(ipAddress);
|
||||
|
||||
if (!_cachedIgnoredIps.Contains(ipAsUint))
|
||||
{
|
||||
ipData.Add(new IpRecord(
|
||||
ipAsUint,
|
||||
now.AddSeconds(-2), // artificially recent
|
||||
CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown"
|
||||
));
|
||||
ipData.Add(new IpRecord(ipAsUint, Time.ActualDateTime().AddSeconds(-2), unknownName));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,20 +537,122 @@ internal class CacheManager: IDisposable
|
|||
if (ipRecord.UsedAt < cutoff || _cachedIgnoredIps.Contains(ipRecord.Ip))
|
||||
continue;
|
||||
|
||||
if (_ipIndex.ContainsKey(ipRecord.Ip))
|
||||
return true;
|
||||
if (!_ipIndex.TryGetValue(ipRecord.Ip, out var banRecords))
|
||||
continue;
|
||||
|
||||
var activeBan = banRecords.FirstOrDefault(r => r.StatusEnum == BanStatus.ACTIVE);
|
||||
if (activeBan == null)
|
||||
continue;
|
||||
|
||||
if (string.IsNullOrEmpty(activeBan.PlayerName))
|
||||
activeBan.PlayerName = unknownName;
|
||||
|
||||
activeBan.PlayerSteamId ??= steamId;
|
||||
|
||||
_ = Task.Run(() => UpdatePlayerData(playerName, steamId, ipAddress));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given IP address is known (previously recorded) for the specified Steam ID.
|
||||
/// </summary>
|
||||
/// <param name="steamId">64-bit Steam ID of the player.</param>
|
||||
/// <param name="ipAddress">IP address to check.</param>
|
||||
/// <returns>True if the IP is recorded for the player, otherwise false.</returns>
|
||||
public bool HasIpForPlayer(ulong steamId, string ipAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
return false;
|
||||
|
||||
if (!IpHelper.TryConvertIpToUint(ipAddress, out var ipUint))
|
||||
return false;
|
||||
|
||||
return _playerIpsCache.TryGetValue(steamId, out var ipData)
|
||||
&& ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress));
|
||||
return _playerIpsCache.TryGetValue(steamId, out var ipData) &&
|
||||
ipData.Contains(new IpRecord(ipUint, default, null!));
|
||||
}
|
||||
|
||||
// public bool HasIpForPlayer(ulong steamId, string ipAddress)
|
||||
// {
|
||||
// if (string.IsNullOrWhiteSpace(ipAddress))
|
||||
// return false;
|
||||
//
|
||||
// return _playerIpsCache.TryGetValue(steamId, out var ipData)
|
||||
// && ipData.Any(x => x.Ip == IpHelper.IpToUint(ipAddress));
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Updates existing active ban records in the database with the latest known player name and IP address.
|
||||
/// Also updates in-memory cache to reflect these changes.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Current player name.</param>
|
||||
/// <param name="steamId">Optional Steam ID of the player.</param>
|
||||
/// <param name="ipAddress">Optional IP address of the player.</param>
|
||||
/// <returns>Asynchronous task representing the update operation.</returns>
|
||||
private async Task UpdatePlayerData(string? playerName, ulong? steamId, string? ipAddress)
|
||||
{
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
return;
|
||||
|
||||
var baseSql = """
|
||||
UPDATE sa_bans
|
||||
SET
|
||||
player_ip = COALESCE(player_ip, @PlayerIP),
|
||||
player_name = COALESCE(player_name, @PlayerName)
|
||||
WHERE
|
||||
(player_steamid = @PlayerSteamID OR player_ip = @PlayerIP)
|
||||
AND status = 'ACTIVE'
|
||||
AND (duration = 0 OR ends > @CurrentTime)
|
||||
""";
|
||||
|
||||
if (!CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
baseSql += " AND server_id = @ServerId;";
|
||||
}
|
||||
|
||||
var parameters = new
|
||||
{
|
||||
PlayerSteamID = steamId,
|
||||
PlayerIP = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|
||||
|| string.IsNullOrEmpty(ipAddress)
|
||||
|| CS2_SimpleAdmin.Instance.Config.OtherSettings.IgnoredIps.Contains(ipAddress)
|
||||
? null
|
||||
: ipAddress,
|
||||
PlayerName = string.IsNullOrEmpty(playerName) ? string.Empty : playerName,
|
||||
CurrentTime = Time.ActualDateTime(),
|
||||
CS2_SimpleAdmin.ServerId
|
||||
};
|
||||
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
await connection.ExecuteAsync(baseSql, parameters);
|
||||
|
||||
if (steamId.HasValue && _steamIdIndex.TryGetValue(steamId.Value, out var steamRecords))
|
||||
{
|
||||
foreach (var rec in steamRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
|
||||
{
|
||||
if (string.IsNullOrEmpty(rec.PlayerIp) && !string.IsNullOrEmpty(ipAddress))
|
||||
rec.PlayerIp = ipAddress;
|
||||
|
||||
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
|
||||
rec.PlayerName = playerName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ipAddress) && IpHelper.TryConvertIpToUint(ipAddress, out var ipUInt)
|
||||
&& _ipIndex.TryGetValue(ipUInt, out var ipRecords))
|
||||
{
|
||||
foreach (var rec in ipRecords.Where(r => r.StatusEnum == BanStatus.ACTIVE))
|
||||
{
|
||||
if (!rec.PlayerSteamId.HasValue && steamId.HasValue)
|
||||
rec.PlayerSteamId = steamId;
|
||||
|
||||
if (string.IsNullOrEmpty(rec.PlayerName) && !string.IsNullOrEmpty(playerName))
|
||||
rec.PlayerName = playerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
|
|
@ -379,6 +665,9 @@ internal class CacheManager: IDisposable
|
|||
_cachedIgnoredIps.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears and disposes of all cached data and marks the object as disposed.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ namespace CS2_SimpleAdmin.Managers;
|
|||
|
||||
public class ChatManager
|
||||
{
|
||||
public void AddChatMessageDB(string playerSteam64, string playerName, string message, bool? team)
|
||||
public void AddChatMessageDB(string playerSteam64, string playerName, string message, bool? team)
|
||||
{
|
||||
if (CS2_SimpleAdmin.Database == null) return;
|
||||
|
||||
Task.Run(async () =>
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = "INSERT INTO `sa_chatlogs` (`playerSteam64`, `playerName`, `message`, `team`, `created`, `serverId`) " +
|
||||
"VALUES (@playerSteam64, @playerName, @message, @team, @created, @serverId)";
|
||||
|
|
|
|||
|
|
@ -1,21 +1,33 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
public class DiscordManager(string webhookUrl)
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Sends a plain text message asynchronously to the configured Discord webhook URL.
|
||||
/// </summary>
|
||||
/// <param name="message">The text message to send to Discord.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public async Task SendMessageAsync(string message)
|
||||
{
|
||||
var client = CS2_SimpleAdmin.HttpClient;
|
||||
|
||||
var payload = new
|
||||
{
|
||||
content = message
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(payload);
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload, options);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
try
|
||||
|
|
@ -34,6 +46,12 @@ public class DiscordManager(string webhookUrl)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an embed message asynchronously to the configured Discord webhook URL.
|
||||
/// </summary>
|
||||
/// <param name="embed">The embed object containing rich content to send.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
||||
public async Task SendEmbedAsync(Embed embed)
|
||||
{
|
||||
var httpClient = CS2_SimpleAdmin.HttpClient;
|
||||
|
|
@ -61,7 +79,12 @@ public class DiscordManager(string webhookUrl)
|
|||
}
|
||||
};
|
||||
|
||||
var jsonPayload = JsonConvert.SerializeObject(payload);
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
var jsonPayload = JsonSerializer.Serialize(payload, options);
|
||||
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await httpClient.PostAsync(webhookUrl, content);
|
||||
|
|
@ -73,6 +96,11 @@ public class DiscordManager(string webhookUrl)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a hexadecimal color string (e.g. "#FF0000") to its integer representation.
|
||||
/// </summary>
|
||||
/// <param name="hex">The hexadecimal color string, optionally starting with '#'.</param>
|
||||
/// <returns>An integer representing the color.</returns>
|
||||
public static int ColorFromHex(string hex)
|
||||
{
|
||||
if (hex.StartsWith($"#"))
|
||||
|
|
@ -84,6 +112,9 @@ public class DiscordManager(string webhookUrl)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Discord embed message containing rich content such as title, description, fields, and images.
|
||||
/// </summary>
|
||||
public class Embed
|
||||
{
|
||||
public int Color { get; init; }
|
||||
|
|
@ -96,6 +127,12 @@ public class Embed
|
|||
|
||||
public List<EmbedField> Fields { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a field to the embed message.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the field.</param>
|
||||
/// <param name="value">The value or content of the field.</param>
|
||||
/// <param name="inline">Whether the field should be displayed inline with other fields.</param>
|
||||
public void AddField(string name, string value, bool inline)
|
||||
{
|
||||
var field = new EmbedField
|
||||
|
|
@ -109,12 +146,18 @@ public class Embed
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the footer section of a Discord embed message, including optional text and icon URL.
|
||||
/// </summary>
|
||||
public class Footer
|
||||
{
|
||||
public string? Text { get; init; }
|
||||
public string? IconUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a field inside a Discord embed message.
|
||||
/// </summary>
|
||||
public class EmbedField
|
||||
{
|
||||
public string? Name { get; init; }
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
using CS2_SimpleAdminApi;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
internal class MuteManager(Database.Database? database)
|
||||
internal class MuteManager(IDatabaseProvider? databaseProvider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a mute entry for a specified player with detailed information.
|
||||
/// </summary>
|
||||
/// <param name="player">Player to be muted.</param>
|
||||
/// <param name="issuer">Admin issuing the mute; null if issued from console.</param>
|
||||
/// <param name="reason">Reason for muting the player.</param>
|
||||
/// <param name="time">Duration of the mute in minutes. Zero means permanent mute.</param>
|
||||
/// <param name="type">Mute type: 0 = GAG, 1 = MUTE, 2 = SILENCE.</param>
|
||||
/// <returns>Mute ID if successfully added, otherwise null.</returns>
|
||||
public async Task<int?> MutePlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
|
||||
{
|
||||
if (database == null) return null;
|
||||
if (databaseProvider == null) return null;
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
|
|
@ -22,21 +32,14 @@ internal class MuteManager(Database.Database? database)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
const string sql = """
|
||||
|
||||
INSERT INTO `sa_mutes`
|
||||
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddMuteQuery(true);
|
||||
|
||||
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = player.SteamId.SteamId64.ToString(),
|
||||
playerSteamid = player.SteamId.SteamId64,
|
||||
playerName = player.Name,
|
||||
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
muteReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -55,10 +58,18 @@ internal class MuteManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<int?> AddMuteBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
|
||||
/// <summary>
|
||||
/// Adds a mute entry for a offline player identified by their SteamID.
|
||||
/// </summary>
|
||||
/// <param name="playerSteamId">SteamID64 of the player to mute.</param>
|
||||
/// <param name="issuer">Admin issuing the mute; can be null if from console.</param>
|
||||
/// <param name="reason">Reason for the mute.</param>
|
||||
/// <param name="time">Mute duration in minutes; 0 for permanent.</param>
|
||||
/// <param name="type">Mute type: 0 = GAG, 1 = MUTE, 2 = SILENCE.</param>
|
||||
/// <returns>Mute ID if successful, otherwise null.</returns>
|
||||
public async Task<int?> AddMuteBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0, int type = 0)
|
||||
{
|
||||
if (database == null) return null;
|
||||
if (string.IsNullOrEmpty(playerSteamId)) return null;
|
||||
if (databaseProvider == null) return null;
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
|
|
@ -72,20 +83,13 @@ internal class MuteManager(Database.Database? database)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
const string sql = """
|
||||
|
||||
INSERT INTO `sa_mutes`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `type`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @type, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddMuteQuery(false);
|
||||
|
||||
var muteId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = playerSteamId,
|
||||
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
muteReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -103,9 +107,14 @@ internal class MuteManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player with the given SteamID currently has any active mutes.
|
||||
/// </summary>
|
||||
/// <param name="steamId">SteamID64 of the player to check.</param>
|
||||
/// <returns>List of active mute records; empty list if none or on error.</returns>
|
||||
public async Task<List<dynamic>> IsPlayerMuted(string steamId)
|
||||
{
|
||||
if (database == null) return [];
|
||||
if (databaseProvider == null) return [];
|
||||
|
||||
if (string.IsNullOrEmpty(steamId))
|
||||
{
|
||||
|
|
@ -119,24 +128,11 @@ internal class MuteManager(Database.Database? database)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var currentTime = Time.ActualDateTime();
|
||||
var sql = "";
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime)"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0))";
|
||||
}
|
||||
else
|
||||
{
|
||||
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1
|
||||
? "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR ends > @CurrentTime) AND server_id = @serverid"
|
||||
: "SELECT * FROM sa_mutes WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' AND (duration = 0 OR duration > COALESCE(passed, 0)) AND server_id = @serverid";
|
||||
|
||||
}
|
||||
|
||||
|
||||
var sql = databaseProvider.GetIsMutedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
|
||||
|
||||
var parameters = new { PlayerSteamID = steamId, CurrentTime = currentTime, serverid = CS2_SimpleAdmin.ServerId };
|
||||
var activeMutes = (await connection.QueryAsync(sql, parameters)).ToList();
|
||||
return activeMutes;
|
||||
|
|
@ -147,35 +143,26 @@ internal class MuteManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves counts of total mutes, gags, and silences for a given player.
|
||||
/// </summary>
|
||||
/// <param name="playerInfo">Information about the player.</param>
|
||||
/// <returns>
|
||||
/// Tuple containing total mutes, total gags, and total silences respectively.
|
||||
/// Returns zeros if no data or on error.
|
||||
/// </returns>
|
||||
public async Task<(int TotalMutes, int TotalGags, int TotalSilences)> GetPlayerMutes(PlayerInfo playerInfo)
|
||||
{
|
||||
if (database == null) return (0,0,0);
|
||||
if (databaseProvider == null) return (0,0,0);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
|
||||
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID;
|
||||
"""
|
||||
: """
|
||||
SELECT
|
||||
COUNT(CASE WHEN type = 'MUTE' THEN 1 END) AS TotalMutes,
|
||||
COUNT(CASE WHEN type = 'GAG' THEN 1 END) AS TotalGags,
|
||||
COUNT(CASE WHEN type = 'SILENCE' THEN 1 END) AS TotalSilences
|
||||
FROM sa_mutes
|
||||
WHERE player_steamid = @PlayerSteamID AND server_id = @ServerId;
|
||||
""";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
var result = await connection.QuerySingleAsync<(int TotalMutes, int TotalGags, int TotalSilences)>(sql, new
|
||||
{
|
||||
PlayerSteamID = playerInfo.SteamId.SteamId64.ToString(),
|
||||
PlayerSteamID = playerInfo.SteamId.SteamId64,
|
||||
CS2_SimpleAdmin.ServerId
|
||||
});
|
||||
|
||||
|
|
@ -187,18 +174,21 @@ internal class MuteManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a batch of online players to update their mute status and remove expired penalties.
|
||||
/// </summary>
|
||||
/// <param name="players">List of tuples containing player SteamID, optional UserID, and slot index.</param>
|
||||
/// <returns>Task representing the asynchronous operation.</returns>
|
||||
public async Task CheckOnlineModeMutes(List<(ulong SteamID, int? UserId, int Slot)> players)
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
const int batchSize = 20;
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE'"
|
||||
: "UPDATE `sa_mutes` SET passed = COALESCE(passed, 0) + 1 WHERE (player_steamid = @PlayerSteamID) AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
var sql = databaseProvider.GetUpdateMutePassedQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
for (var i = 0; i < players.Count; i += batchSize)
|
||||
{
|
||||
|
|
@ -213,10 +203,7 @@ internal class MuteManager(Database.Database? database)
|
|||
await connection.ExecuteAsync(sql, parametersList);
|
||||
}
|
||||
|
||||
sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE'"
|
||||
: "SELECT * FROM `sa_mutes` WHERE player_steamid = @PlayerSteamID AND passed >= duration AND duration > 0 AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
|
||||
sql = databaseProvider.GetCheckExpiredMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
|
||||
foreach (var (steamId, _, slot) in players)
|
||||
{
|
||||
|
|
@ -233,9 +220,17 @@ internal class MuteManager(Database.Database? database)
|
|||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes active mutes for players matching the specified pattern.
|
||||
/// </summary>
|
||||
/// <param name="playerPattern">Pattern to match player names or identifiers.</param>
|
||||
/// <param name="adminSteamId">SteamID64 of the admin performing the unmute.</param>
|
||||
/// <param name="reason">Reason for unmuting the player(s).</param>
|
||||
/// <param name="type">Mute type to remove: 0 = GAG, 1 = MUTE, 2 = SILENCE.</param>
|
||||
/// <returns>Task representing the asynchronous operation.</returns>
|
||||
public async Task UnmutePlayer(string playerPattern, string adminSteamId, string reason, int type = 0)
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
if (playerPattern.Length <= 1)
|
||||
{
|
||||
|
|
@ -244,8 +239,7 @@ internal class MuteManager(Database.Database? database)
|
|||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var muteType = type switch
|
||||
{
|
||||
1 => "MUTE",
|
||||
|
|
@ -253,27 +247,16 @@ internal class MuteManager(Database.Database? database)
|
|||
_ => "GAG"
|
||||
};
|
||||
|
||||
string sqlRetrieveMutes;
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
sqlRetrieveMutes = "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND " +
|
||||
"type = @muteType AND status = 'ACTIVE'";
|
||||
}
|
||||
else
|
||||
{
|
||||
sqlRetrieveMutes = "SELECT id FROM sa_mutes WHERE (player_steamid = @pattern OR player_name = @pattern) AND " +
|
||||
"type = @muteType AND status = 'ACTIVE' AND server_id = @serverid";
|
||||
}
|
||||
|
||||
var sqlRetrieveMutes =
|
||||
databaseProvider.GetRetrieveMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
var mutes = await connection.QueryAsync(sqlRetrieveMutes, new { pattern = playerPattern, muteType, serverid = CS2_SimpleAdmin.ServerId });
|
||||
|
||||
var mutesList = mutes as dynamic[] ?? mutes.ToArray();
|
||||
if (mutesList.Length == 0)
|
||||
return;
|
||||
|
||||
const string sqlAdmin = "SELECT id FROM sa_admins WHERE player_steamid = @adminSteamId";
|
||||
var sqlInsertUnmute = "INSERT INTO sa_unmutes (mute_id, admin_id, reason) VALUES (@muteId, @adminId, @reason); SELECT LAST_INSERT_ID();";
|
||||
var sqlAdmin = databaseProvider.GetUnmuteAdminIdQuery();
|
||||
var sqlInsertUnmute = databaseProvider.GetInsertUnmuteQuery(string.IsNullOrEmpty(reason));
|
||||
|
||||
var sqlAdminId = await connection.ExecuteScalarAsync<int?>(sqlAdmin, new { adminSteamId });
|
||||
var adminId = sqlAdminId ?? 0;
|
||||
|
|
@ -281,21 +264,11 @@ internal class MuteManager(Database.Database? database)
|
|||
foreach (var mute in mutesList)
|
||||
{
|
||||
int muteId = mute.id;
|
||||
int? unmuteId;
|
||||
|
||||
// Insert into sa_unmutes
|
||||
if (reason != null)
|
||||
{
|
||||
unmuteId = await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
|
||||
}
|
||||
else
|
||||
{
|
||||
sqlInsertUnmute = "INSERT INTO sa_unmutes (muteId, admin_id) VALUES (@muteId, @adminId); SELECT LAST_INSERT_ID();";
|
||||
unmuteId = await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId });
|
||||
}
|
||||
int? unmuteId =
|
||||
await connection.ExecuteScalarAsync<int>(sqlInsertUnmute, new { muteId, adminId, reason });
|
||||
|
||||
// Update sa_mutes to set unmute_id
|
||||
const string sqlUpdateMute = "UPDATE sa_mutes SET status = 'UNMUTED', unmute_id = @unmuteId WHERE id = @muteId";
|
||||
var sqlUpdateMute = databaseProvider.GetUpdateMuteStatusQuery();
|
||||
await connection.ExecuteAsync(sqlUpdateMute, new { unmuteId, muteId });
|
||||
}
|
||||
}
|
||||
|
|
@ -305,28 +278,18 @@ internal class MuteManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expires all old mutes that have passed their duration according to current time.
|
||||
/// </summary>
|
||||
/// <returns>Task representing the asynchronous expiration operation.</returns>
|
||||
public async Task ExpireOldMutes()
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
string sql;
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1
|
||||
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
|
||||
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration`";
|
||||
}
|
||||
else
|
||||
{
|
||||
sql = CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 1
|
||||
? "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid"
|
||||
: "UPDATE sa_mutes SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND `passed` >= `duration` AND server_id = @serverid";
|
||||
}
|
||||
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetExpireMutesQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode);
|
||||
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,284 +11,372 @@ using ZLinq;
|
|||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
public class PlayerManager
|
||||
internal class PlayerManager
|
||||
{
|
||||
private readonly SemaphoreSlim _loadPlayerSemaphore = new(5);
|
||||
private readonly CS2_SimpleAdminConfig _config = CS2_SimpleAdmin.Instance.Config;
|
||||
|
||||
/// <summary>
|
||||
/// Loads and initializes player data when a client connects.
|
||||
/// </summary>
|
||||
/// <param name="player">The <see cref="CCSPlayerController"/> instance representing the connecting player.</param>
|
||||
/// <param name="fullConnect">
|
||||
/// Determines whether to perform a full synchronization of player data.
|
||||
/// If true, full checks (bans, IP history, penalties, warns, mutes) will be loaded and applied.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This method validates the player's identity, checks for bans, updates the IP history table,
|
||||
/// loads penalties (mutes/gags/warns), and optionally notifies admin players about the connecting player's penalties.
|
||||
/// </remarks>
|
||||
public void LoadPlayerData(CCSPlayerController player, bool fullConnect = false)
|
||||
{
|
||||
if (player.IsBot || string.IsNullOrEmpty(player.IpAddress) || player.IpAddress.Contains("127.0.0.1"))
|
||||
return;
|
||||
|
||||
if (!player.UserId.HasValue)
|
||||
{
|
||||
Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
|
||||
return;
|
||||
}
|
||||
|
||||
var ipAddress = player.IpAddress?.Split(":")[0];
|
||||
CS2_SimpleAdmin.PlayersInfo[player.UserId.Value] =
|
||||
new PlayerInfo(player.UserId.Value, player.Slot, new SteamID(player.SteamID), player.PlayerName, ipAddress);
|
||||
|
||||
// if (!player.UserId.HasValue)
|
||||
// {
|
||||
// Helper.KickPlayer(player, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
|
||||
// return;
|
||||
// }
|
||||
|
||||
var userId = player.UserId.Value;
|
||||
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(userId))
|
||||
{
|
||||
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_INVALIDCONNECTION);
|
||||
}
|
||||
|
||||
var steamId64 = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64;
|
||||
var steamId = steamId64.ToString();
|
||||
|
||||
if (CS2_SimpleAdmin.Database == null) return;
|
||||
var slot = player.Slot;
|
||||
var steamId = player.SteamID;
|
||||
var playerName = !string.IsNullOrEmpty(player.PlayerName)
|
||||
? player.PlayerName
|
||||
: CS2_SimpleAdmin._localizer?["sa_unknown"] ?? "Unknown";
|
||||
var ipAddress = player.IpAddress?.Split(":")[0];
|
||||
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null || CS2_SimpleAdmin.Instance.CacheManager == null) return;
|
||||
|
||||
// Perform asynchronous database operations within a single method
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var isBanned = CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
try
|
||||
{
|
||||
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(steamId, null),
|
||||
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
||||
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(steamId64, ipAddress)
|
||||
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(steamId, ipAddress)
|
||||
};
|
||||
await _loadPlayerSemaphore.WaitAsync();
|
||||
|
||||
if (isBanned)
|
||||
{
|
||||
// Kick the player if banned
|
||||
await Server.NextFrameAsync(() =>
|
||||
if (!CS2_SimpleAdmin.PlayersInfo.ContainsKey(steamId))
|
||||
{
|
||||
if (!player.UserId.HasValue) return;
|
||||
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64, ipAddress))
|
||||
var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
|
||||
|
||||
const string updateQuery = """
|
||||
UPDATE `sa_players_ips`
|
||||
SET used_at = CURRENT_TIMESTAMP
|
||||
WHERE steamid = @SteamID AND address = @IPAddress;
|
||||
""";
|
||||
await connection.ExecuteAsync(updateQuery, new
|
||||
{
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
|
||||
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, null),
|
||||
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
||||
? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId,
|
||||
ipAddress)
|
||||
: CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(playerName, steamId, ipAddress)
|
||||
};
|
||||
|
||||
const string selectQuery =
|
||||
"SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
|
||||
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
|
||||
// CS2_SimpleAdmin._logger?.LogInformation($"Player {playerName} ({steamId} - {ipAddress}) is banned? {isBanned.ToString()}");
|
||||
|
||||
if (isBanned)
|
||||
{
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
// CS2_SimpleAdmin._logger?.LogInformation($"Kicking {playerName}");
|
||||
Helper.KickPlayer(userId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED);
|
||||
});
|
||||
|
||||
if (recordExists > 0)
|
||||
{
|
||||
const string updateQuery = """
|
||||
UPDATE `sa_players_ips`
|
||||
SET used_at = CURRENT_TIMESTAMP
|
||||
WHERE steamid = @SteamID AND address = @IPAddress;
|
||||
""";
|
||||
await connection.ExecuteAsync(updateQuery, new
|
||||
{
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
const string insertQuery = """
|
||||
INSERT INTO `sa_players_ips` (steamid, address, used_at)
|
||||
VALUES (@SteamID, @IPAddress, CURRENT_TIMESTAMP);
|
||||
""";
|
||||
await connection.ExecuteAsync(insertQuery, new
|
||||
{
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError(
|
||||
$"Unable to save ip address for {CS2_SimpleAdmin.PlayersInfo[userId].Name} ({ipAddress}) {ex.Message}");
|
||||
}
|
||||
|
||||
// Get all accounts associated to the player (ip address)
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].AccountsAssociated =
|
||||
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable().Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|
||||
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(
|
||||
// CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null)
|
||||
// : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
||||
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin
|
||||
// .PlayersInfo[userId].SteamId.SteamId64)
|
||||
// : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress);
|
||||
|
||||
if (fullConnect || !fullConnect) // Temp skip
|
||||
{
|
||||
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(CS2_SimpleAdmin.PlayersInfo[userId], false);
|
||||
var (totalMutes, totalGags, totalSilences) =
|
||||
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(CS2_SimpleAdmin.PlayersInfo[userId]);
|
||||
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalBans = CS2_SimpleAdmin.Instance.CacheManager?.GetPlayerBansBySteamId(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString()).Count ?? 0;
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes = totalMutes;
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalGags = totalGags;
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences = totalSilences;
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns = warns.Count;
|
||||
|
||||
// Check if the player is muted
|
||||
var activeMutes = await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString());
|
||||
|
||||
if (activeMutes.Count > 0)
|
||||
{
|
||||
foreach (var mute in activeMutes)
|
||||
{
|
||||
string muteType = mute.type;
|
||||
DateTime ends = mute.ends;
|
||||
int duration = mute.duration;
|
||||
switch (muteType)
|
||||
{
|
||||
// Apply mute penalty based on mute type
|
||||
case "GAG":
|
||||
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Gag, ends, duration);
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
|
||||
break;
|
||||
case "MUTE":
|
||||
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Mute, ends, duration);
|
||||
await Server.NextFrameAsync(() =>
|
||||
{
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
});
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]);
|
||||
break;
|
||||
default:
|
||||
PlayerPenaltyManager.AddPenalty(CS2_SimpleAdmin.PlayersInfo[userId].Slot, PenaltyType.Silence, ends, duration);
|
||||
await Server.NextFrameAsync(() =>
|
||||
{
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
});
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect && fullConnect)
|
||||
if (fullConnect)
|
||||
{
|
||||
var associatedAcccountsChunks = CS2_SimpleAdmin.PlayersInfo[userId].AccountsAssociated.ChunkBy(5).ToList();
|
||||
|
||||
await Server.NextFrameAsync(() =>
|
||||
{
|
||||
foreach (var admin in Helper.GetValidPlayers()
|
||||
.Where(p => (AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/kick") ||
|
||||
AdminManager.PlayerHasPermissions(new SteamID(p.SteamID), "@css/ban")) &&
|
||||
p.Connected == PlayerConnectedState.PlayerConnected && !CS2_SimpleAdmin.AdminDisabledJoinComms.Contains(p.SteamID)))
|
||||
{
|
||||
if (CS2_SimpleAdmin._localizer != null && admin != player)
|
||||
{
|
||||
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_penalty_info",
|
||||
player.PlayerName,
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalBans,
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalGags,
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalMutes,
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalSilences,
|
||||
CS2_SimpleAdmin.PlayersInfo[userId].TotalWarns
|
||||
);
|
||||
var playerInfo = new PlayerInfo(userId, slot, new SteamID(steamId), playerName, ipAddress);
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId] = playerInfo;
|
||||
|
||||
foreach (var chunk in associatedAcccountsChunks)
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
if (!CS2_SimpleAdmin.CachedPlayers.Contains(player))
|
||||
CS2_SimpleAdmin.CachedPlayers.Add(player);
|
||||
});
|
||||
|
||||
if (_config.OtherSettings.CheckMultiAccountsByIp && ipAddress != null &&
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId] != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.CacheManager.HasIpForPlayer(
|
||||
steamId, ipAddress))
|
||||
{
|
||||
const string updateQuery = """
|
||||
UPDATE `sa_players_ips`
|
||||
SET used_at = CURRENT_TIMESTAMP,
|
||||
name = @playerName
|
||||
WHERE steamid = @SteamID AND address = @IPAddress;
|
||||
""";
|
||||
await connection.ExecuteAsync(updateQuery, new
|
||||
{
|
||||
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer, "sa_admin_associated_accounts",
|
||||
player.PlayerName,
|
||||
string.Join(", ",
|
||||
chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))
|
||||
);
|
||||
playerName,
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
const string selectQuery =
|
||||
"SELECT COUNT(*) FROM `sa_players_ips` WHERE steamid = @SteamID AND address = @IPAddress;";
|
||||
var recordExists = await connection.ExecuteScalarAsync<int>(selectQuery, new
|
||||
{
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
|
||||
if (recordExists > 0)
|
||||
{
|
||||
const string updateQuery = """
|
||||
UPDATE `sa_players_ips`
|
||||
SET used_at = CURRENT_TIMESTAMP,
|
||||
name = @playerName
|
||||
WHERE steamid = @SteamID AND address = @IPAddress;
|
||||
""";
|
||||
await connection.ExecuteAsync(updateQuery, new
|
||||
{
|
||||
playerName,
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
const string insertQuery = """
|
||||
INSERT INTO `sa_players_ips` (steamid, name, address, used_at)
|
||||
VALUES (@SteamID, @playerName, @IPAddress, CURRENT_TIMESTAMP);
|
||||
""";
|
||||
await connection.ExecuteAsync(insertQuery, new
|
||||
{
|
||||
SteamID = CS2_SimpleAdmin.PlayersInfo[steamId].SteamId.SteamId64,
|
||||
playerName,
|
||||
IPAddress = IpHelper.IpToUint(ipAddress)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError(
|
||||
$"Unable to save ip address for {playerInfo.Name} ({ipAddress}) {ex.Message}");
|
||||
}
|
||||
|
||||
playerInfo.AccountsAssociated =
|
||||
CS2_SimpleAdmin.Instance.CacheManager?.GetAccountsByIp(ipAddress).AsValueEnumerable()
|
||||
.Select(x => (x.SteamId, x.PlayerName)).ToList() ?? [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// var isBanned = CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType == 0
|
||||
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(
|
||||
// CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), null)
|
||||
// : CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
||||
// ? CS2_SimpleAdmin.Instance.CacheManager.IsPlayerOrAnyIpBanned(CS2_SimpleAdmin
|
||||
// .PlayersInfo[userId].SteamId.SteamId64)
|
||||
// : CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(CS2_SimpleAdmin.PlayersInfo[userId].SteamId.SteamId64.ToString(), ipAddress);
|
||||
|
||||
if (CS2_SimpleAdmin.PlayersInfo.TryGetValue(steamId, out PlayerInfo? value)) // Temp skip
|
||||
{
|
||||
var warns = await CS2_SimpleAdmin.Instance.WarnManager.GetPlayerWarns(value, false);
|
||||
var (totalMutes, totalGags, totalSilences) =
|
||||
await CS2_SimpleAdmin.Instance.MuteManager.GetPlayerMutes(value);
|
||||
value.TotalBans = CS2_SimpleAdmin.Instance.CacheManager
|
||||
?.GetPlayerBansBySteamId(value.SteamId.SteamId64)
|
||||
.Count ?? 0;
|
||||
value.TotalMutes = totalMutes;
|
||||
value.TotalGags = totalGags;
|
||||
value.TotalSilences = totalSilences;
|
||||
value.TotalWarns = warns.Count;
|
||||
|
||||
var activeMutes =
|
||||
await CS2_SimpleAdmin.Instance.MuteManager.IsPlayerMuted(value.SteamId.SteamId64
|
||||
.ToString());
|
||||
|
||||
if (activeMutes.Count > 0)
|
||||
{
|
||||
foreach (var mute in activeMutes)
|
||||
{
|
||||
string muteType = mute.type;
|
||||
DateTime ends = mute.ends;
|
||||
int duration = mute.duration;
|
||||
switch (muteType)
|
||||
{
|
||||
// Apply mute penalty based on mute type
|
||||
case "GAG":
|
||||
PlayerPenaltyManager.AddPenalty(
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
||||
PenaltyType.Gag, ends, duration);
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Gag].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_gag", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
|
||||
break;
|
||||
case "MUTE":
|
||||
PlayerPenaltyManager.AddPenalty(
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
||||
PenaltyType.Mute, ends, duration);
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
});
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Mute].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_mute", ends.ToLocalTime().ToString(CultureInfo.InvariantCulture)]);
|
||||
break;
|
||||
default:
|
||||
PlayerPenaltyManager.AddPenalty(
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].Slot,
|
||||
PenaltyType.Silence, ends, duration);
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
player.VoiceFlags = VoiceFlags.Muted;
|
||||
});
|
||||
// if (CS2_SimpleAdmin._localizer != null)
|
||||
// mutesList[PenaltyType.Silence].Add(CS2_SimpleAdmin._localizer["sa_player_penalty_info_active_silence", ends.ToLocalTime().ToString(CultureInfo.CurrentCulture)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.NotifyPenaltiesToAdminOnConnect)
|
||||
{
|
||||
await Server.NextWorldUpdateAsync(() =>
|
||||
{
|
||||
foreach (var admin in Helper.GetValidPlayers()
|
||||
.Where(p => (AdminManager.PlayerHasPermissions(
|
||||
new SteamID(p.SteamID),
|
||||
"@css/kick") ||
|
||||
AdminManager.PlayerHasPermissions(
|
||||
new SteamID(p.SteamID),
|
||||
"@css/ban")) &&
|
||||
p.Connected == PlayerConnectedState.PlayerConnected &&
|
||||
!CS2_SimpleAdmin.AdminDisabledJoinComms
|
||||
.Contains(p.SteamID)))
|
||||
{
|
||||
if (CS2_SimpleAdmin._localizer == null || admin == player) continue;
|
||||
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
|
||||
"sa_admin_penalty_info",
|
||||
player.PlayerName,
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].TotalBans,
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].TotalGags,
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].TotalMutes,
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].TotalSilences,
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].TotalWarns
|
||||
);
|
||||
|
||||
if (CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.Count >= 2)
|
||||
{
|
||||
var associatedAcccountsChunks =
|
||||
CS2_SimpleAdmin.PlayersInfo[steamId].AccountsAssociated.ChunkBy(5)
|
||||
.ToList();
|
||||
foreach (var chunk in associatedAcccountsChunks)
|
||||
{
|
||||
admin.SendLocalizedMessage(CS2_SimpleAdmin._localizer,
|
||||
"sa_admin_associated_accounts",
|
||||
player.PlayerName,
|
||||
string.Join(", ",
|
||||
chunk.Select(a => $"{a.PlayerName} ({a.SteamId})"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}",
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
finally
|
||||
{
|
||||
CS2_SimpleAdmin._logger?.LogError("Error processing player connection: {exception}", ex.Message);
|
||||
_loadPlayerSemaphore.Release();
|
||||
}
|
||||
});
|
||||
|
||||
if (CS2_SimpleAdmin.RenamedPlayers.TryGetValue(player.SteamID, out var name))
|
||||
{
|
||||
player.Rename(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodically checks the status of online players and applies timers for speed, gravity,
|
||||
/// and penalty expiration validation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method registers two repeating timers:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>One short-interval timer to update speed/gravity modifications applied to players.</description></item>
|
||||
/// <item><description>
|
||||
/// One long-interval timer (default 61 seconds) to expire bans, mutes, warns, refresh caches,
|
||||
/// and remove outdated penalties from connected players.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// Additionally, banned players still online are kicked, and admins may be updated about mute statuses based on the configured time mode.
|
||||
/// </remarks>
|
||||
public void CheckPlayersTimer()
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddTimer(0.1f, () =>
|
||||
CS2_SimpleAdmin.Instance.AddTimer(0.12f, () =>
|
||||
{
|
||||
if (CS2_SimpleAdmin.GravityPlayers.Count <= 0) return;
|
||||
|
||||
foreach (var value in CS2_SimpleAdmin.GravityPlayers.Where(value => value.Key is
|
||||
{ IsValid: true, Connected: PlayerConnectedState.PlayerConnected } || value.Key.PlayerPawn?.Value?.LifeState != (int)LifeState_t.LIFE_ALIVE))
|
||||
if (CS2_SimpleAdmin.SpeedPlayers.Count > 0)
|
||||
{
|
||||
value.Key.SetGravity(value.Value);
|
||||
foreach (var (slot, speed) in CS2_SimpleAdmin.SpeedPlayers)
|
||||
{
|
||||
var player = Utilities.GetPlayerFromSlot(slot);
|
||||
|
||||
if (player is null) continue;
|
||||
|
||||
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
|
||||
{
|
||||
player.SetSpeed(speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (CS2_SimpleAdmin.GravityPlayers.Count > 0)
|
||||
{
|
||||
foreach (var (slot, gravity) in CS2_SimpleAdmin.GravityPlayers)
|
||||
{
|
||||
var player = Utilities.GetPlayerFromSlot(slot);
|
||||
|
||||
if (player is null) continue;
|
||||
|
||||
if (player is { IsValid: true, Connected: PlayerConnectedState.PlayerConnected, PlayerPawn.Value.LifeState: (int)LifeState_t.LIFE_ALIVE })
|
||||
{
|
||||
player.SetGravity(gravity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, TimerFlags.REPEAT);
|
||||
|
||||
CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
|
||||
|
||||
CS2_SimpleAdmin.Instance.PlayersTimer = CS2_SimpleAdmin.Instance.AddTimer(61.0f, () =>
|
||||
{
|
||||
#if DEBUG
|
||||
CS2_SimpleAdmin._logger?.LogCritical("[OnMapStart] Expired check");
|
||||
#endif
|
||||
if (CS2_SimpleAdmin.Database == null)
|
||||
if (CS2_SimpleAdmin.DatabaseProvider == null)
|
||||
return;
|
||||
|
||||
|
||||
var tempPlayers = Helper.GetValidPlayers()
|
||||
.Select(p => new
|
||||
{
|
||||
p.SteamID, p.IpAddress, p.UserId, p.Slot,
|
||||
p.PlayerName,
|
||||
p.SteamID,
|
||||
p.IpAddress,
|
||||
p.UserId,
|
||||
p.Slot,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
|
||||
var pluginInstance = CS2_SimpleAdmin.Instance;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var expireTasks = new Task[]
|
||||
var expireTasks = new[]
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.BanManager.ExpireOldBans(),
|
||||
CS2_SimpleAdmin.Instance.MuteManager.ExpireOldMutes(),
|
||||
CS2_SimpleAdmin.Instance.WarnManager.ExpireOldWarns(),
|
||||
CS2_SimpleAdmin.Instance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask,
|
||||
CS2_SimpleAdmin.Instance.PermissionManager.DeleteOldAdmins()
|
||||
pluginInstance.BanManager.ExpireOldBans(),
|
||||
pluginInstance.MuteManager.ExpireOldMutes(),
|
||||
pluginInstance.WarnManager.ExpireOldWarns(),
|
||||
pluginInstance.CacheManager?.RefreshCacheAsync() ?? Task.CompletedTask,
|
||||
pluginInstance.PermissionManager.DeleteOldAdmins()
|
||||
};
|
||||
|
||||
await Task.WhenAll(expireTasks);
|
||||
|
|
@ -305,56 +393,66 @@ public class PlayerManager
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (pluginInstance.CacheManager == null)
|
||||
return;
|
||||
|
||||
var bannedPlayers = tempPlayers.AsValueEnumerable()
|
||||
.Where(player =>
|
||||
{
|
||||
return CS2_SimpleAdmin.Instance.CacheManager != null && CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
var playerName = player.PlayerName;
|
||||
var steamId = player.SteamID;
|
||||
var ip = player.IpAddress?.Split(':')[0];
|
||||
|
||||
return CS2_SimpleAdmin.Instance.Config.OtherSettings.BanType switch
|
||||
{
|
||||
0 => CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(player.SteamID.ToString(), null),
|
||||
_ =>
|
||||
CS2_SimpleAdmin.Instance.CacheManager.IsPlayerBanned(player.SteamID.ToString(), player.IpAddress?.Split(":")[0])
|
||||
0 => pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, null),
|
||||
_ => CS2_SimpleAdmin.Instance.Config.OtherSettings.CheckMultiAccountsByIp
|
||||
? pluginInstance.CacheManager.IsPlayerOrAnyIpBanned(playerName, steamId, ip)
|
||||
: pluginInstance.CacheManager.IsPlayerBanned(playerName, steamId, ip)
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var player in bannedPlayers)
|
||||
}).ToList();
|
||||
|
||||
if (bannedPlayers.Count > 0)
|
||||
{
|
||||
if (player.UserId.HasValue)
|
||||
await Server.NextFrameAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED));
|
||||
foreach (var player in bannedPlayers)
|
||||
{
|
||||
if (!player.UserId.HasValue) continue;
|
||||
await Server.NextWorldUpdateAsync(() => Helper.KickPlayer((int)player.UserId, NetworkDisconnectionReason.NETWORK_DISCONNECT_REJECT_BANNED));
|
||||
}
|
||||
}
|
||||
|
||||
var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList();
|
||||
if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return;
|
||||
|
||||
if (_config.OtherSettings.TimeMode == 0)
|
||||
{
|
||||
await CS2_SimpleAdmin.Instance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
|
||||
var onlinePlayers = tempPlayers.AsValueEnumerable().Select(player => (player.SteamID, player.UserId, player.Slot)).ToList();
|
||||
if (tempPlayers.Count == 0 || onlinePlayers.Count == 0) return;
|
||||
await pluginInstance.MuteManager.CheckOnlineModeMutes(onlinePlayers);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var players = Helper.GetValidPlayers();
|
||||
var penalizedSlots = players
|
||||
.Where(player => PlayerPenaltyManager.IsSlotInPenalties(player.Slot))
|
||||
.Select(player => new
|
||||
{
|
||||
Player = player,
|
||||
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
|
||||
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
|
||||
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
|
||||
.Select(player => new
|
||||
{
|
||||
Player = player,
|
||||
IsMuted = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Mute, out _),
|
||||
IsSilenced = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Silence, out _),
|
||||
IsGagged = PlayerPenaltyManager.IsPenalized(player.Slot, PenaltyType.Gag, out _)
|
||||
});
|
||||
|
||||
foreach (var entry in penalizedSlots)
|
||||
foreach (var entry in penalizedSlots)
|
||||
{
|
||||
// If the player is not muted or silenced, set voice flags to normal
|
||||
if (!entry.IsMuted && !entry.IsSilenced)
|
||||
{
|
||||
// If the player is not muted or silenced, set voice flags to normal
|
||||
if (!entry.IsMuted && !entry.IsSilenced)
|
||||
{
|
||||
entry.Player.VoiceFlags = VoiceFlags.Normal;
|
||||
if (entry.Player.VoiceFlags.HasFlag(VoiceFlags.Muted))
|
||||
entry.Player.VoiceFlags &= ~VoiceFlags.Muted;
|
||||
}
|
||||
entry.Player.VoiceFlags = VoiceFlags.Normal;
|
||||
if (entry.Player.VoiceFlags.HasFlag(VoiceFlags.Muted))
|
||||
entry.Player.VoiceFlags &= ~VoiceFlags.Muted;
|
||||
}
|
||||
}
|
||||
|
||||
PlayerPenaltyManager.RemoveExpiredPenalties();
|
||||
}
|
||||
|
|
@ -362,7 +460,6 @@ public class PlayerManager
|
|||
{
|
||||
CS2_SimpleAdmin._logger?.LogError($"Unable to remove old penalties: {ex.Message}");
|
||||
}
|
||||
|
||||
}, TimerFlags.REPEAT);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,13 @@ public static class PlayerPenaltyManager
|
|||
private static readonly ConcurrentDictionary<int, Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>> Penalties =
|
||||
new();
|
||||
|
||||
// Add a penalty for a player
|
||||
/// <summary>
|
||||
/// Adds a penalty for a specific player slot and penalty type.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot where the penalty should be applied.</param>
|
||||
/// <param name="penaltyType">The type of penalty to apply (e.g. gag, mute, silence).</param>
|
||||
/// <param name="endDateTime">The validity expiration date/time of the penalty.</param>
|
||||
/// <param name="durationInMinutes">The duration of the penalty in minutes (0 for permanent).</param>
|
||||
public static void AddPenalty(int slot, PenaltyType penaltyType, DateTime endDateTime, int durationInMinutes)
|
||||
{
|
||||
Penalties.AddOrUpdate(slot,
|
||||
|
|
@ -33,6 +39,13 @@ public static class PlayerPenaltyManager
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a player is currently penalized with the given penalty type.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot to check.</param>
|
||||
/// <param name="penaltyType">The penalty type to check.</param>
|
||||
/// <param name="endDateTime">The out-parameter returning the end datetime of the penalty if active.</param>
|
||||
/// <returns>True if the player has an active penalty, false otherwise.</returns>
|
||||
public static bool IsPenalized(int slot, PenaltyType penaltyType, out DateTime? endDateTime)
|
||||
{
|
||||
endDateTime = null;
|
||||
|
|
@ -74,7 +87,12 @@ public static class PlayerPenaltyManager
|
|||
return false;
|
||||
}
|
||||
|
||||
// Get the end datetime and duration of penalties for a player and penalty type
|
||||
/// <summary>
|
||||
/// Retrieves all penalties for a player of a specific penalty type.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
/// <param name="penaltyType">The penalty type to retrieve.</param>
|
||||
/// <returns>A list of penalties if found, otherwise an empty list.</returns>
|
||||
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, PenaltyType penaltyType)
|
||||
{
|
||||
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
|
||||
|
|
@ -85,6 +103,35 @@ public static class PlayerPenaltyManager
|
|||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all penalties for a player across multiple penalty types.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
/// <param name="penaltyType">A list of penalty types to retrieve.</param>
|
||||
/// <returns>A combined list of penalties of all requested types.</returns>
|
||||
public static List<(DateTime EndDateTime, int Duration, bool Passed)> GetPlayerPenalties(int slot, List<PenaltyType> penaltyType)
|
||||
{
|
||||
List<(DateTime EndDateTime, int Duration, bool Passed)> result = [];
|
||||
|
||||
if (Penalties.TryGetValue(slot, out var penaltyDict))
|
||||
{
|
||||
foreach (var type in penaltyType)
|
||||
{
|
||||
if (penaltyDict.TryGetValue(type, out var penaltiesList))
|
||||
{
|
||||
result.AddRange(penaltiesList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all penalties for a player across all penalty types.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
/// <returns>A dictionary with penalty types as keys and lists of penalties as values.</returns>
|
||||
public static Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>> GetAllPlayerPenalties(int slot)
|
||||
{
|
||||
// Check if the player has any penalties in the dictionary
|
||||
|
|
@ -95,12 +142,20 @@ public static class PlayerPenaltyManager
|
|||
new Dictionary<PenaltyType, List<(DateTime EndDateTime, int Duration, bool Passed)>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a given slot has any penalties assigned.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
/// <returns>True if the player has any penalties, false otherwise.</returns>
|
||||
public static bool IsSlotInPenalties(int slot)
|
||||
{
|
||||
return Penalties.ContainsKey(slot);
|
||||
}
|
||||
|
||||
// Remove all penalties for a player slot
|
||||
/// <summary>
|
||||
/// Removes all penalties assigned to a specific player slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
public static void RemoveAllPenalties(int slot)
|
||||
{
|
||||
if (Penalties.ContainsKey(slot))
|
||||
|
|
@ -109,13 +164,19 @@ public static class PlayerPenaltyManager
|
|||
}
|
||||
}
|
||||
|
||||
// Remove all penalties
|
||||
/// <summary>
|
||||
/// Removes all penalties for all players.
|
||||
/// </summary>
|
||||
public static void RemoveAllPenalties()
|
||||
{
|
||||
Penalties.Clear();
|
||||
}
|
||||
|
||||
// Remove all penalties of a selected type from a specific player
|
||||
/// <summary>
|
||||
/// Removes all penalties of a specific type from a player.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
/// <param name="penaltyType">The penalty type to remove.</param>
|
||||
public static void RemovePenaltiesByType(int slot, PenaltyType penaltyType)
|
||||
{
|
||||
if (Penalties.TryGetValue(slot, out var penaltyDict) &&
|
||||
|
|
@ -125,6 +186,11 @@ public static class PlayerPenaltyManager
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks penalties with a specific end datetime as "passed" for a player.
|
||||
/// </summary>
|
||||
/// <param name="slot">The player slot.</param>
|
||||
/// <param name="dateTime">The end datetime of penalties to mark as passed.</param>
|
||||
public static void RemovePenaltiesByDateTime(int slot, DateTime dateTime)
|
||||
{
|
||||
if (!Penalties.TryGetValue(slot, out var penaltyDict)) return;
|
||||
|
|
@ -146,7 +212,13 @@ public static class PlayerPenaltyManager
|
|||
}
|
||||
}
|
||||
|
||||
// Remove all expired penalties for all players and penalty types
|
||||
/// <summary>
|
||||
/// Removes or expires penalties automatically across all players based on their duration or "passed" flag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <c>TimeMode == 0</c>, penalties are considered passed manually and are removed if flagged as such.
|
||||
/// Otherwise, expired penalties are removed based on the current datetime compared with their end time.
|
||||
/// </remarks>
|
||||
public static void RemoveExpiredPenalties()
|
||||
{
|
||||
if (CS2_SimpleAdmin.Instance.Config.OtherSettings.TimeMode == 0)
|
||||
|
|
@ -172,13 +244,11 @@ public static class PlayerPenaltyManager
|
|||
var now = Time.ActualDateTime();
|
||||
foreach (var (playerSlot, penaltyDict) in Penalties.ToList()) // Use ToList to avoid modification while iterating
|
||||
{
|
||||
// Remove expired penalties for the player
|
||||
foreach (var penaltiesList in penaltyDict.Values)
|
||||
{
|
||||
penaltiesList.RemoveAll(p => p.Duration > 0 && now >= p.EndDateTime);
|
||||
}
|
||||
|
||||
// Remove player slot if no penalties left
|
||||
if (penaltyDict.Count == 0)
|
||||
{
|
||||
Penalties.TryRemove(playerSlot, out _);
|
||||
|
|
|
|||
|
|
@ -9,21 +9,31 @@ public class ServerManager
|
|||
{
|
||||
private int _getIpTryCount;
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the server setting <c>sv_hibernate_when_empty</c> is enabled.
|
||||
/// Logs an error if this setting is true, since it prevents the plugin from working properly.
|
||||
/// </summary>
|
||||
public static void CheckHibernationStatus()
|
||||
{
|
||||
var convar = ConVar.Find("sv_hibernate_when_empty");
|
||||
|
||||
if (convar == null || !convar.GetPrimitiveValue<bool>())
|
||||
return;
|
||||
|
||||
|
||||
CS2_SimpleAdmin._logger?.LogError("Detected setting \"sv_hibernate_when_empty true\", set false to make plugin work properly");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates the asynchronous process to load server data such as IP address, port, hostname, and RCON password.
|
||||
/// Handles retry attempts if IP address is not immediately available.
|
||||
/// Updates or inserts the server record in the database accordingly.
|
||||
/// After loading, triggers admin reload and cache initialization.
|
||||
/// Also optionally sends plugin usage metrics if enabled in configuration.
|
||||
/// </summary>
|
||||
public void LoadServerData()
|
||||
{
|
||||
CS2_SimpleAdmin.Instance.AddTimer(1.2f, () =>
|
||||
CS2_SimpleAdmin.Instance.AddTimer(2.0f, () =>
|
||||
{
|
||||
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.ServerId != null || CS2_SimpleAdmin.Database == null) return;
|
||||
if (CS2_SimpleAdmin.ServerLoaded || CS2_SimpleAdmin.DatabaseProvider == null) return;
|
||||
|
||||
if (_getIpTryCount > 32 && Helper.GetServerIp().StartsWith("0.0.0.0") || string.IsNullOrEmpty(Helper.GetServerIp()))
|
||||
{
|
||||
|
|
@ -32,7 +42,6 @@ public class ServerManager
|
|||
}
|
||||
|
||||
var ipAddress = ConVar.Find("ip")?.StringValue;
|
||||
|
||||
if (string.IsNullOrEmpty(ipAddress) || ipAddress.StartsWith("0.0.0"))
|
||||
{
|
||||
ipAddress = Helper.GetServerIp();
|
||||
|
|
@ -58,7 +67,7 @@ public class ServerManager
|
|||
{
|
||||
try
|
||||
{
|
||||
await using var connection = await CS2_SimpleAdmin.Database.GetConnectionAsync();
|
||||
await using var connection = await CS2_SimpleAdmin.DatabaseProvider.CreateConnectionAsync();
|
||||
var addressExists = await connection.ExecuteScalarAsync<bool>(
|
||||
"SELECT COUNT(*) FROM sa_servers WHERE address = @address",
|
||||
new { address });
|
||||
|
|
@ -67,11 +76,11 @@ public class ServerManager
|
|||
{
|
||||
string query = "INSERT INTO sa_servers (address, hostname) VALUES (@address, @hostname)";
|
||||
|
||||
if(CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
|
||||
if (CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
|
||||
{
|
||||
query = "INSERT INTO sa_servers (address, hostname, rcon) VALUES (@address, @hostname, @rcon)";
|
||||
}
|
||||
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
query,
|
||||
new { address, hostname, rcon });
|
||||
|
|
@ -80,11 +89,11 @@ public class ServerManager
|
|||
{
|
||||
string query = "UPDATE `sa_servers` SET `hostname` = @hostname, `id` = `id` WHERE `address` = @address";
|
||||
|
||||
if(CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
|
||||
if (CS2_SimpleAdmin.Instance.Config.IsCSSPanel)
|
||||
{
|
||||
query = "UPDATE `sa_servers` SET `hostname` = @hostname, rcon = @rcon, `id` = `id` WHERE `address` = @address";
|
||||
}
|
||||
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
query,
|
||||
new { address, rcon, hostname });
|
||||
|
|
@ -95,7 +104,6 @@ public class ServerManager
|
|||
new { address });
|
||||
|
||||
CS2_SimpleAdmin.ServerId = serverId;
|
||||
|
||||
CS2_SimpleAdmin._logger?.LogInformation("Loaded server with ip {ip}", ipAddress);
|
||||
|
||||
if (CS2_SimpleAdmin.ServerId != null)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,37 @@
|
|||
using CS2_SimpleAdminApi;
|
||||
using CS2_SimpleAdmin.Database;
|
||||
using CS2_SimpleAdminApi;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CS2_SimpleAdmin.Managers;
|
||||
|
||||
internal class WarnManager(Database.Database? database)
|
||||
internal class WarnManager(IDatabaseProvider? databaseProvider)
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a warning to a player with an optional issuer and reason.
|
||||
/// </summary>
|
||||
/// <param name="player">The player who is being warned.</param>
|
||||
/// <param name="issuer">The player issuing the warning; null indicates console or system.</param>
|
||||
/// <param name="reason">The reason for the warning.</param>
|
||||
/// <param name="time">Optional duration of the warning in minutes (0 means permanent).</param>
|
||||
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
|
||||
public async Task<int?> WarnPlayer(PlayerInfo player, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (database == null) return null;
|
||||
if (databaseProvider == null) return null;
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
const string sql = """
|
||||
|
||||
INSERT INTO `sa_warns`
|
||||
(`player_steamid`, `player_name`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @playerName, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddWarnQuery(true);
|
||||
|
||||
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = player.SteamId.SteamId64.ToString(),
|
||||
playerSteamid = player.SteamId.SteamId64,
|
||||
playerName = player.Name,
|
||||
adminSteamid = issuer?.SteamId.SteamId64.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
muteReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -46,30 +48,30 @@ internal class WarnManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<int?> AddWarnBySteamid(string playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
|
||||
/// <summary>
|
||||
/// Adds a warning to a player identified by SteamID with optional issuer and reason.
|
||||
/// </summary>
|
||||
/// <param name="playerSteamId">The SteamID64 of the player being warned.</param>
|
||||
/// <param name="issuer">The player issuing the warning; null indicates console or system.</param>
|
||||
/// <param name="reason">The reason for the warning.</param>
|
||||
/// <param name="time">Optional duration of the warning in minutes (0 means permanent).</param>
|
||||
/// <returns>The identifier of the inserted warning, or null if the operation failed.</returns>
|
||||
public async Task<int?> AddWarnBySteamid(ulong playerSteamId, PlayerInfo? issuer, string reason, int time = 0)
|
||||
{
|
||||
if (database == null) return null;
|
||||
if (string.IsNullOrEmpty(playerSteamId)) return null;
|
||||
if (databaseProvider == null) return null;
|
||||
|
||||
var now = Time.ActualDateTime();
|
||||
var futureTime = now.AddMinutes(time);
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
const string sql = """
|
||||
|
||||
INSERT INTO `sa_warns`
|
||||
(`player_steamid`, `admin_steamid`, `admin_name`, `reason`, `duration`, `ends`, `created`, `server_id`)
|
||||
VALUES
|
||||
(@playerSteamid, @adminSteamid, @adminName, @muteReason, @duration, @ends, @created, @serverid);
|
||||
SELECT LAST_INSERT_ID();
|
||||
""";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
var sql = databaseProvider.GetAddWarnQuery(false);
|
||||
|
||||
var warnId = await connection.ExecuteScalarAsync<int?>(sql, new
|
||||
{
|
||||
playerSteamid = playerSteamId,
|
||||
adminSteamid = issuer?.SteamId.ToString() ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
adminSteamid = issuer?.SteamId.SteamId64 ?? 0,
|
||||
adminName = issuer?.Name ?? CS2_SimpleAdmin._localizer?["sa_console"] ?? "Console",
|
||||
muteReason = reason,
|
||||
duration = time,
|
||||
|
|
@ -86,30 +88,22 @@ internal class WarnManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of warnings for a specific player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player whose warnings to retrieve.</param>
|
||||
/// <param name="active">If true, returns only active (non-expired) warnings; otherwise returns all warnings.</param>
|
||||
/// <returns>A list of dynamic objects representing warnings, or an empty list if none found or on failure.</returns>
|
||||
public async Task<List<dynamic>> GetPlayerWarns(PlayerInfo player, bool active = true)
|
||||
{
|
||||
if (database == null) return [];
|
||||
if (databaseProvider == null) return [];
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
string sql;
|
||||
|
||||
if (CS2_SimpleAdmin.Instance.Config.MultiServerMode)
|
||||
{
|
||||
sql = active
|
||||
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE' ORDER BY id DESC"
|
||||
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID ORDER BY id DESC";
|
||||
}
|
||||
else
|
||||
{
|
||||
sql = active
|
||||
? "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE' ORDER BY id DESC"
|
||||
: "SELECT * FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid ORDER BY id DESC";
|
||||
}
|
||||
|
||||
var parameters = new { PlayerSteamID = player.SteamId.SteamId64.ToString(), serverid = CS2_SimpleAdmin.ServerId };
|
||||
var sql = databaseProvider.GetPlayerWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
|
||||
var parameters = new { PlayerSteamID = player.SteamId.SteamId64, serverid = CS2_SimpleAdmin.ServerId };
|
||||
var warns = await connection.QueryAsync<dynamic>(sql, parameters);
|
||||
|
||||
return warns.ToList();
|
||||
|
|
@ -120,24 +114,23 @@ internal class WarnManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
public async Task<int> GetPlayerWarnsCount(string steamId, bool active = true)
|
||||
/// <summary>
|
||||
/// Retrieves the count of warnings for a player specified by SteamID.
|
||||
/// </summary>
|
||||
/// <param name="steamId">The SteamID64 of the player.</param>
|
||||
/// <param name="active">If true, counts only active (non-expired) warnings; otherwise counts all warnings.</param>
|
||||
/// <returns>The count of warnings as an integer, or 0 if none found or on failure.</returns>
|
||||
public async Task<int> GetPlayerWarnsCount(ulong steamId, bool active = true)
|
||||
{
|
||||
if (database == null) return 0;
|
||||
if (databaseProvider == null) return 0;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? active
|
||||
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND status = 'ACTIVE'"
|
||||
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID"
|
||||
: active
|
||||
? "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID AND server_id = @serverid AND status = 'ACTIVE'"
|
||||
: "SELECT COUNT(*) FROM sa_warns WHERE player_steamid = @PlayerSteamID'";
|
||||
|
||||
var muteCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
return muteCount;
|
||||
var sql = databaseProvider.GetPlayerWarnsCountQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode, active);
|
||||
var warnsCount = await connection.ExecuteScalarAsync<int>(sql, new { PlayerSteamID = steamId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
return warnsCount;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
@ -145,19 +138,22 @@ internal class WarnManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a specific warning by its identifier from a player's record.
|
||||
/// </summary>
|
||||
/// <param name="player">The player whose warning will be removed.</param>
|
||||
/// <param name="warnId">The identifier of the warning to remove.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task UnwarnPlayer(PlayerInfo player, int warnId)
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId"
|
||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND player_steamid = @steamid AND id = @warnId AND server_id = @serverid";
|
||||
|
||||
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64.ToString(), warnId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
var sql = databaseProvider.GetUnwarnByIdQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { steamid = player.SteamId.SteamId64, warnId, serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -165,36 +161,20 @@ internal class WarnManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the most recent warning matching a player pattern (usually SteamID string).
|
||||
/// </summary>
|
||||
/// <param name="playerPattern">The pattern identifying the player whose last warning should be removed.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task UnwarnPlayer(string playerPattern)
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
|
||||
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? """
|
||||
UPDATE sa_warns
|
||||
JOIN (
|
||||
SELECT MAX(id) AS max_id
|
||||
FROM sa_warns
|
||||
WHERE player_steamid = @steamid AND status = 'ACTIVE'
|
||||
) AS subquery ON sa_warns.id = subquery.max_id
|
||||
SET sa_warns.status = 'EXPIRED'
|
||||
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid;
|
||||
"""
|
||||
: """
|
||||
UPDATE sa_warns
|
||||
JOIN (
|
||||
SELECT MAX(id) AS max_id
|
||||
FROM sa_warns
|
||||
WHERE player_steamid = @steamid AND status = 'ACTIVE' AND server_id = @serverid
|
||||
) AS subquery ON sa_warns.id = subquery.max_id
|
||||
SET sa_warns.status = 'EXPIRED'
|
||||
WHERE sa_warns.status = 'ACTIVE' AND sa_warns.player_steamid = @steamid AND sa_warns.server_id = @serverid;
|
||||
""";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetUnwarnLastQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { steamid = playerPattern, serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -203,18 +183,19 @@ internal class WarnManager(Database.Database? database)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expires old warnings based on the current time, removing or marking them as inactive.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task ExpireOldWarns()
|
||||
{
|
||||
if (database == null) return;
|
||||
if (databaseProvider == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var connection = await database.GetConnectionAsync();
|
||||
|
||||
var sql = CS2_SimpleAdmin.Instance.Config.MultiServerMode
|
||||
? "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime"
|
||||
: "UPDATE sa_warns SET status = 'EXPIRED' WHERE status = 'ACTIVE' AND `duration` > 0 AND ends <= @CurrentTime AND server_id = @serverid";
|
||||
await using var connection = await databaseProvider.CreateConnectionAsync();
|
||||
|
||||
var sql = databaseProvider.GetExpireWarnsQuery(CS2_SimpleAdmin.Instance.Config.MultiServerMode);
|
||||
await connection.ExecuteAsync(sql, new { CurrentTime = Time.ActualDateTime(), serverid = CS2_SimpleAdmin.ServerId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue