Compare commits

...

18 Commits

Author SHA1 Message Date
Deserty0
aa621ece26 Merge remote-tracking branch 'upstream/master' into funnyfunnystuff 2025-08-23 19:44:16 +10:00
Deserty0
52880de47e должно теперь работать 2025-08-13 21:07:21 +10:00
Deserty0
eb289104ab Update DiscordAuthManager.cs 2025-08-13 05:40:08 +10:00
Deserty0
db49a55481 Update CCvars.CP14DiscordAuth.cs 2025-08-13 05:40:08 +10:00
Deserty0
f01725fdc7 Update CCvars.CP14DiscordAuth.cs 2025-08-13 05:15:57 +10:00
Deserty0
0a6a560066 Update DiscordAuthManager.cs 2025-08-13 05:04:38 +10:00
Deserty0
6f170bb45f lol 2025-08-13 04:55:57 +10:00
Deserty0
cec07307bf 123 2025-08-13 04:49:12 +10:00
Deserty0
bd80a1eb60 Update PanicBunkerTab.xaml 2025-08-13 04:49:12 +10:00
Deserty0
b0c2b92464 Update DiscordAuthManager.cs 2025-08-13 04:15:38 +10:00
Deserty0
35e5025d3e Update CCvars.CP14DiscordAuth.cs 2025-08-13 04:15:37 +10:00
Deserty0
146967b786 looool 2025-08-13 04:15:37 +10:00
Deserty0
4b68d79729 uaua 2025-08-13 04:15:37 +10:00
Deserty0
8899695ed5 Update DiscordAuthManager.cs 2025-08-13 04:15:37 +10:00
Deserty0
1d410590b6 Update DiscordAuthManager.cs 2025-08-13 04:15:37 +10:00
Deserty0
24eb9d7493 Update DiscordAuthManager.cs 2025-08-13 04:15:37 +10:00
Deserty0
565283ae26 Update DiscordAuthManager.cs 2025-08-13 04:15:36 +10:00
Deserty0
d55800c575 funny 2025-08-13 04:15:36 +10:00
14 changed files with 280 additions and 20 deletions

View File

@@ -12,7 +12,7 @@ public sealed partial class AdminMenuWindow : DefaultWindow
public AdminMenuWindow()
{
MinSize = new Vector2(650, 250);
MinSize = new Vector2(650, 280);
Title = Loc.GetString("admin-menu-title");
RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle((int) TabIndex.Admin, Loc.GetString("admin-menu-admin-tab"));

View File

@@ -27,7 +27,12 @@
<cc:CommandButton Name="ShowReasonButton" Command="panicbunker_show_reason"
ToggleMode="True" Text="{Loc admin-ui-panic-bunker-show-reason}"
ToolTip="{Loc admin-ui-panic-bunker-show-reason-tooltip}" />
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc cp14-admin-ui-suspicious-warning-level-setting}" MinWidth="175" />
<OptionButton Name="SuspiciousWarningLevel" HorizontalAlignment="Left" Margin="4 0"
ToolTip="{Loc cp14-admin-ui-suspicious-warning-level-setting-desc}" />
</BoxContainer>
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" Margin="2">
<Label Text="{Loc admin-ui-panic-bunker-min-account-age}" MinWidth="175" />
<LineEdit Name="MinAccountAge" MinWidth="50" Margin="0 0 5 0" />

View File

@@ -1,7 +1,9 @@
using Content.Shared.Administration.Events;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Client.Administration.UI.Tabs.PanicBunkerTab;
@@ -10,9 +12,13 @@ namespace Content.Client.Administration.UI.Tabs.PanicBunkerTab;
public sealed partial class PanicBunkerTab : Control
{
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private string _minAccountAge;
private string _minOverallMinutes;
// CrystallEdge suspicious activity warning system
private List<SuspiciousWarningLevelSelection> _suspiciousWarningLevelSelections = [];
private string _currentSuspiciousWarningLevel = string.Empty;
public PanicBunkerTab()
{
@@ -28,6 +34,21 @@ public sealed partial class PanicBunkerTab : Control
MinOverallMinutes.OnTextEntered += args => SendMinOverallMinutes(args.Text);
MinOverallMinutes.OnFocusExit += args => SendMinOverallMinutes(args.Text);
_minOverallMinutes = MinOverallMinutes.Text;
// CrystallEdge suspicious activity warning system
foreach (var type in Enum.GetValues<SuspiciousWarningLevelSelection>())
{
_suspiciousWarningLevelSelections.Add(type);
SuspiciousWarningLevel.AddItem(GetLocalizedEnumValue(type));
}
UpdateSuspiciousWarningLevel();
SuspiciousWarningLevel.OnItemSelected += ev =>
{
SuspiciousWarningLevel.SelectId(ev.Id);
SendSuspiciousWarningLevel(_suspiciousWarningLevelSelections[ev.Id]);
};
}
private void SendMinAccountAge(string text)
@@ -74,4 +95,43 @@ public sealed partial class PanicBunkerTab : Control
MinOverallMinutes.Text = status.MinOverallMinutes.ToString();
_minOverallMinutes = MinOverallMinutes.Text;
}
// CrystallEdge suspicious activity warning system
private enum SuspiciousWarningLevelSelection
{
Disabled,
Low,
Medium,
High,
}
private string GetLocalizedEnumValue(SuspiciousWarningLevelSelection selection)
{
return selection switch
{
SuspiciousWarningLevelSelection.Disabled => Loc.GetString("cp14-admin-ui-suspicious-warning-level-disabled"),
SuspiciousWarningLevelSelection.Low => Loc.GetString("cp14-admin-ui-suspicious-warning-level-low"),
SuspiciousWarningLevelSelection.Medium => Loc.GetString("cp14-admin-ui-suspicious-warning-level-medium"),
SuspiciousWarningLevelSelection.High => Loc.GetString("cp14-admin-ui-suspicious-warning-level-high"),
_ => throw new ArgumentOutOfRangeException(nameof(selection), selection, null),
};
}
private void UpdateSuspiciousWarningLevel()
{
_currentSuspiciousWarningLevel = _cfg.GetCVar(CCVars.SuspiciousAccountsWarningLevel);
if (!Enum.TryParse(_currentSuspiciousWarningLevel, true, out SuspiciousWarningLevelSelection currentSuspiciousWarningSelection))
{
throw new ArgumentOutOfRangeException(nameof(_currentSuspiciousWarningLevel),
_currentSuspiciousWarningLevel,
null);
}
SuspiciousWarningLevel.SelectId((int)currentSuspiciousWarningSelection);
}
private void SendSuspiciousWarningLevel(SuspiciousWarningLevelSelection selection)
{
_console.ExecuteCommand($"cp14.suspicious-warning-level {Enum.GetName(selection)}");
}
}

View File

@@ -0,0 +1,40 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Server._CP14.Administration.Commands;
[AdminCommand(AdminFlags.Server)]
public sealed class SuspiciousLevelCommand : LocalizedCommands
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
public override string Command => "cp14.suspicious-warning-level";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
var level = _cfg.GetCVar(CCVars.SuspiciousAccountsWarningLevel);
shell.WriteLine(Loc.GetString("cp14-suspicious-warning-level-command-current", ("level", level)));
}
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
return;
}
List<string> possibleLevels = ["disabled", "low", "medium", "high"];
if (!possibleLevels.Contains(args[0].ToLower()))
{
shell.WriteError(Loc.GetString("cp14-suspicious-warning-level-command-error"));
return;
}
_cfg.SetCVar(CCVars.SuspiciousAccountsWarningLevel, args[0]);
shell.WriteLine(Loc.GetString("cp14-suspicious-warning-level-command-set", ("level", args[0])));
}
}

View File

@@ -29,8 +29,13 @@ public sealed class DiscordAuthManager
private bool _enabled;
private string _apiUrl = string.Empty;
private string _apiKey = string.Empty;
// Suspicious activity blocking stuff
private string _suspiciousAccountsWarningLevel = string.Empty;
private bool _panicBunkerEnabled;
private string _panicBunkerCustomReason = string.Empty;
private bool _panicBunkerShowReason;
public const string DISCORD_GUILD = "1221923073759121468"; //CrystallEdge server required
public const string RequiredDiscordGuild = "1221923073759121468"; //CrystallEdge server required
private HashSet<string> _blockedGuilds = new()
{
@@ -54,6 +59,11 @@ public sealed class DiscordAuthManager
_cfg.OnValueChanged(CCVars.DiscordAuthEnabled, v => _enabled = v, true);
_cfg.OnValueChanged(CCVars.DiscordAuthUrl, v => _apiUrl = v, true);
_cfg.OnValueChanged(CCVars.DiscordAuthToken, v => _apiKey = v, true);
// Suspicious activity blocking stuff
_cfg.OnValueChanged(CCVars.SuspiciousAccountsWarningLevel, v => _suspiciousAccountsWarningLevel = v, true);
_cfg.OnValueChanged(CCVars.PanicBunkerEnabled, v => _panicBunkerEnabled = v, true);
_cfg.OnValueChanged(CCVars.PanicBunkerCustomReason, v => _panicBunkerCustomReason = v, true);
_cfg.OnValueChanged(CCVars.PanicBunkerShowReason, v => _panicBunkerShowReason = v, true);
_netMgr.RegisterNetMessage<MsgDiscordAuthRequired>();
_netMgr.RegisterNetMessage<MsgDiscordAuthCheck>(OnAuthCheck);
@@ -113,26 +123,103 @@ public sealed class DiscordAuthManager
_sawmill.Debug($"{await response.Content.ReadAsStringAsync(cancel)}");
_sawmill.Debug($"{(int)response.StatusCode}");
var verified = response.StatusCode == HttpStatusCode.OK;
var guildsVerified = await CheckGuilds(userId, cancel);
if (!verified)
return new AuthData { Verified = false, ErrorMessage = Loc.GetString("cp14-discord-info")};
return guildsVerified;
var userVerified = await VerifyUser(userId, cancel);
return userVerified;
}
private async Task<AuthData> CheckGuilds(NetUserId userId, CancellationToken cancel = default)
private async Task<AuthData> VerifyUser(NetUserId userId, CancellationToken cancel = default)
{
var isSuspicious = false;
var isWhitelisted = await _db.GetWhitelistStatusAsync(userId);
if (isWhitelisted)
{
_sawmill.Debug($"{userId} is whitelisted, Verified");
return new AuthData { Verified = true };
}
var guilds = await GetUserGuilds(userId, cancel);
if (guilds.Guilds is [])
{
return new AuthData { Verified = false, ErrorMessage = guilds.ErrorMessage };
}
foreach (var guild in guilds.Guilds)
{
if (_blockedGuilds.Contains(guild.Id))
{
_sawmill.Debug($"{userId} exist in blocked guild {guild.Id}, Suspicious");
isSuspicious = true;
break;
}
}
if (guilds.Guilds.All(guild => guild.Id != RequiredDiscordGuild))
{
_sawmill.Debug($"Player {userId} is not in required guild {RequiredDiscordGuild}");
return new AuthData { Verified = false, ErrorMessage = "You are not a member of the CrystallEdge server." };
}
var user = await GetDiscordUser(userId, cancel);
if (user.Id == string.Empty)
{
return new AuthData { Verified = false, ErrorMessage = user.ErrorMessage };
}
var accountAge = GetAccountAge(user.Id);
if (accountAge < 45)
{
_sawmill.Debug($"{userId} have account age lower than 45 days, Suspicious");
isSuspicious = true;
}
// Fastest way to block user is just not verify it
switch (_suspiciousAccountsWarningLevel.ToLower())
{
case "medium":
{
if (_panicBunkerEnabled)
{
var errorMessage =
System.Text.Encoding.UTF8.GetString(Convert.FromBase64String("RXJyb3IgMjcwMQ=="));
if (_panicBunkerShowReason)
{
errorMessage = "Panic bunker enabled";
if (_panicBunkerCustomReason != string.Empty)
{
errorMessage = _panicBunkerCustomReason;
}
}
_sawmill.Debug($"{userId} is suspicious, warning level medium and panic bunker enabled, Not Verified");
return new AuthData { Verified = false, ErrorMessage = errorMessage };
}
_sawmill.Debug($"{userId} is suspicious, warning level medium but panic bunker disabled, Verified");
break;
}
case "high":
{
_sawmill.Debug($"{userId} is suspicious and warning level high, Not Verified");
return new AuthData
{
Verified = false,
ErrorMessage = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String("RXJyb3IgMjcwMQ=="))
};
}
}
return new AuthData { Verified = true, Suspicious = isSuspicious };
}
private async Task<DiscordGuildsResponse> GetUserGuilds(NetUserId userId, CancellationToken cancel = default)
{
_sawmill.Debug($"Checking guilds for {userId}");
var requestUrl = $"{_apiUrl}/api/guilds?method=uid&id={userId}";
_sawmill.Debug($"Guilds request url:{requestUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
@@ -141,33 +228,60 @@ public sealed class DiscordAuthManager
_sawmill.Debug($"(int) response.StatusCode: {(int)response.StatusCode}");
if (!response.IsSuccessStatusCode)
{
_sawmill.Debug($"Player {userId} guilds check failed: !response.IsSuccessStatusCode");
return new AuthData { Verified = false, ErrorMessage = "Unexpected error: !response.IsSuccessStatusCode" };
_sawmill.Debug($"Player {userId} guilds check failed: {(int)response.StatusCode}");
return new DiscordGuildsResponse { ErrorMessage = $"Unexpected error: {(int)response.StatusCode}" };
}
var guilds = await response.Content.ReadFromJsonAsync<DiscordGuildsResponse>(cancel);
if (guilds is null)
{
_sawmill.Debug($"Player {userId} guilds check failed: guilds is null");
return new AuthData { Verified = false, ErrorMessage = "Unexpected error: guilds is null" };
return new DiscordGuildsResponse { ErrorMessage = "Unexpected error: guilds is null" };
}
_sawmill.Debug($"Player {userId} guilds check succeed.");
return new DiscordGuildsResponse { Guilds = guilds.Guilds };
}
foreach (var guild in guilds.Guilds)
private async Task<DiscordUserResponse> GetDiscordUser(NetUserId userId, CancellationToken cancel = default)
{
if (_blockedGuilds.Contains(guild.Id))
_sawmill.Debug($"Checking account age for {userId}");
var requestUrl = $"{_apiUrl}/api/identify?method=uid&id={userId}";
_sawmill.Debug($"User request url:{requestUrl}");
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
var response = await _httpClient.SendAsync(request, cancel);
_sawmill.Debug($"(int) response.StatusCode: {(int)response.StatusCode}");
if (!response.IsSuccessStatusCode)
{
var errorMessage = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String("RXJyb3IgMjcwMQ=="));
return new AuthData { Verified = false, ErrorMessage = errorMessage };
}
_sawmill.Debug($"Player {userId} user age check failed: {(int)response.StatusCode}");
return new DiscordUserResponse { ErrorMessage = $"Unexpected error: {(int)response.StatusCode}" };
}
if (guilds.Guilds.All(guild => guild.Id != DISCORD_GUILD))
var user = await response.Content.ReadFromJsonAsync<DiscordUserResponse>(cancel);
if (user is null)
{
_sawmill.Debug($"Player {userId} is not in required guild {DISCORD_GUILD}");
return new AuthData { Verified = false, ErrorMessage = "You are not a member of the CrystallEdge server." };
_sawmill.Debug($"Player {userId} user age check failed: user is null");
return new DiscordUserResponse { ErrorMessage = "Unexpected error: user is null" };
}
_sawmill.Debug($"Player {userId} user id get succeed.");
return new DiscordUserResponse { Id = user.Id };
}
return new AuthData { Verified = true };
private double GetAccountAge(string id)
{
// Please check https://discord.com/developers/docs/reference#convert-snowflake-to-datetime
var unixEpoch = 1420070400000;
var intId = Convert.ToInt32(id);
var snowflakeCreationDateBin = Convert.ToString(intId, 2).Substring(42);
var snowflakeCreationDateDecimal = Convert.ToInt32(snowflakeCreationDateBin) + unixEpoch;
var accountCreationDate = DateTime.UnixEpoch.AddSeconds(snowflakeCreationDateDecimal);
var accountAge = DateTime.Now.Subtract(accountCreationDate);
return accountAge.TotalDays;
}
public async Task<string?> GenerateLink(NetUserId userId, CancellationToken cancel = default)
@@ -208,6 +322,14 @@ public sealed class DiscordAuthManager
{
[JsonPropertyName("guilds")]
public DiscordGuild[] Guilds { get; set; } = [];
public string ErrorMessage { get; set; } = string.Empty;
}
private sealed class DiscordUserResponse
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
}
private sealed class DiscordGuild
@@ -220,5 +342,6 @@ public sealed class DiscordAuthManager
{
public bool Verified { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
public bool Suspicious { get; set; }
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server._CP14.Discord;
using Content.Server.Chat.Managers;
using Content.Server.Connection;
using Content.Shared._CP14.JoinQueue;
using Content.Shared.CCVar;
@@ -40,6 +41,7 @@ public sealed class JoinQueueManager
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly DiscordAuthManager _discordAuthManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
private ISawmill _sawmill = default!;
/// <summary>
@@ -48,6 +50,7 @@ public sealed class JoinQueueManager
private readonly List<ICommonSession> _queue = new(); // Real Queue class can't delete disconnected users
private bool _isEnabled = false;
private string _suspiciousAccountWarningLevel = string.Empty;
public int PlayerInQueueCount => _queue.Count;
public int ActualPlayersCount => _playerManager.PlayerCount - PlayerInQueueCount; // Now it's only real value with actual players count that in game
@@ -57,6 +60,7 @@ public sealed class JoinQueueManager
_netManager.RegisterNetMessage<MsgQueueUpdate>();
_cfg.OnValueChanged(CCVars.QueueEnabled, OnQueueCVarChanged, true);
_cfg.OnValueChanged(CCVars.SuspiciousAccountsWarningLevel, v => _suspiciousAccountWarningLevel = v, true);
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_discordAuthManager.PlayerVerified += OnPlayerVerified;
_sawmill = Logger.GetSawmill("queue");
@@ -168,5 +172,10 @@ public sealed class JoinQueueManager
private void SendToGame(ICommonSession s)
{
Timer.Spawn(0, () => _playerManager.JoinGame(s));
// Suspicious account warning
if (_suspiciousAccountWarningLevel.ToLower() != "disabled")
{
_chatManager.SendAdminAnnouncement(Loc.GetString("cp14-suspicious-player-join-message", ("name", s.Name)));
}
}
}

View File

@@ -55,7 +55,7 @@ public sealed class SponsorSystem : ICP14SponsorManager
private async Task<List<string>?> GetRoles(NetUserId userId)
{
var requestUrl = $"{_apiUrl}/api/roles?method=uid&id={userId}&guildId={DiscordAuthManager.DISCORD_GUILD}";
var requestUrl = $"{_apiUrl}/api/roles?method=uid&id={userId}&guildId={DiscordAuthManager.RequiredDiscordGuild}";
var response = await _httpClient.GetAsync(requestUrl);
if (!response.IsSuccessStatusCode)

View File

@@ -12,4 +12,7 @@ public sealed partial class CCVars
public static readonly CVarDef<string> DiscordAuthToken =
CVarDef.Create("cp14.discord_auth_token", "token", CVar.SERVERONLY | CVar.CONFIDENTIAL);
public static readonly CVarDef<string> SuspiciousAccountsWarningLevel =
CVarDef.Create("cp14.suspicious_accounts_warning_level", "disabled", CVar.SERVER | CVar.CONFIDENTIAL | CVar.ARCHIVE);
}

View File

@@ -0,0 +1,3 @@
cp14-suspicious-warning-level-command-error = Warning level can be only disabled, low, medium or high
cp14-suspicious-warning-level-command-current = Current warning level is {$level}
cp14-suspicious-warning-level-command-set = Warning level set to {$level}

View File

@@ -0,0 +1,6 @@
cp14-admin-ui-suspicious-warning-level-setting = Warning level
cp14-admin-ui-suspicious-warning-level-setting-desc = Level of response to players with suspicious discord accounts
cp14-admin-ui-suspicious-warning-level-disabled = Disabled
cp14-admin-ui-suspicious-warning-level-low = Low
cp14-admin-ui-suspicious-warning-level-medium = Medium
cp14-admin-ui-suspicious-warning-level-high = High

View File

@@ -0,0 +1 @@
cp14-suspicious-player-join-message = SUSPICIOUS player {$name} joined.

View File

@@ -0,0 +1,3 @@
cp14-suspicious-warning-level-command-error = Уровень тревоги может быть только disabled, low, medium или high
cp14-suspicious-warning-level-command-current = Текущий уровень тревоги {$level}
cp14-suspicious-warning-level-command-set = Уровень тревоги установлен на {$level}

View File

@@ -0,0 +1 @@
cp14-suspicious-player-join-message = ПОДОЗРИТЕЛЬНЫЙ игрок {$name} зашёл.

View File

@@ -0,0 +1,6 @@
cp14-admin-ui-suspicious-warning-level-setting = Уровень тревоги
cp14-admin-ui-suspicious-warning-level-setting-desc = Уровень реакции на игроков с подозрительными дискорд аккаунтами
cp14-admin-ui-suspicious-warning-level-disabled = Нет
cp14-admin-ui-suspicious-warning-level-low = Низкий
cp14-admin-ui-suspicious-warning-level-medium = Средний
cp14-admin-ui-suspicious-warning-level-high = Высокий