+++ /dev/null
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Security.Cryptography;
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading.Tasks;
-using Content.Server.Administration.Systems;
-using Content.Server.GameTicking;
-using Content.Server.GameTicking.Presets;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Maps;
-using Content.Server.RoundEnd;
-using Content.Shared.Administration.Events;
-using Content.Shared.Administration.Managers;
-using Content.Shared.CCVar;
-using Content.Shared.Prototypes;
-using Robust.Server.ServerStatus;
-using Robust.Shared.Asynchronous;
-using Robust.Shared.Configuration;
-using Robust.Shared.Network;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Administration;
-
-public sealed class ServerApi : IPostInjectInit
-{
- [Dependency] private readonly IStatusHost _statusHost = default!;
- [Dependency] private readonly IConfigurationManager _config = default!;
- [Dependency] private readonly ISharedPlayerManager _playerManager = default!; // Players
- [Dependency] private readonly ISharedAdminManager _adminManager = default!; // Admins
- [Dependency] private readonly IGameMapManager _gameMapManager = default!; // Map name
- [Dependency] private readonly IServerNetManager _netManager = default!; // Kick
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Game rules
- [Dependency] private readonly IComponentFactory _componentFactory = default!;
- [Dependency] private readonly ITaskManager _taskManager = default!; // game explodes when calling stuff from the non-game thread
- [Dependency] private readonly EntityManager _entityManager = default!;
-
- [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
-
- private string _token = string.Empty;
- private ISawmill _sawmill = default!;
-
- public static Dictionary<string, string> PanicPunkerCvarNames = new()
- {
- { "Enabled", "game.panic_bunker.enabled" },
- { "DisableWithAdmins", "game.panic_bunker.disable_with_admins" },
- { "EnableWithoutAdmins", "game.panic_bunker.enable_without_admins" },
- { "CountDeadminnedAdmins", "game.panic_bunker.count_deadminned_admins" },
- { "ShowReason", "game.panic_bunker.show_reason" },
- { "MinAccountAgeHours", "game.panic_bunker.min_account_age" },
- { "MinOverallHours", "game.panic_bunker.min_overall_hours" },
- { "CustomReason", "game.panic_bunker.custom_reason" }
- };
-
- void IPostInjectInit.PostInject()
- {
- _sawmill = Logger.GetSawmill("serverApi");
-
- // Get
- _statusHost.AddHandler(InfoHandler);
- _statusHost.AddHandler(GetGameRules);
- _statusHost.AddHandler(GetForcePresets);
-
- // Post
- _statusHost.AddHandler(ActionRoundStatus);
- _statusHost.AddHandler(ActionKick);
- _statusHost.AddHandler(ActionAddGameRule);
- _statusHost.AddHandler(ActionEndGameRule);
- _statusHost.AddHandler(ActionForcePreset);
- _statusHost.AddHandler(ActionForceMotd);
- _statusHost.AddHandler(ActionPanicPunker);
- }
-
- public void Initialize()
- {
- _config.OnValueChanged(CCVars.AdminApiToken, UpdateToken, true);
- }
-
- public void Shutdown()
- {
- _config.UnsubValueChanged(CCVars.AdminApiToken, UpdateToken);
- }
-
- private void UpdateToken(string token)
- {
- _token = token;
- }
-
-
-#region Actions
-
- /// <summary>
- /// Changes the panic bunker settings.
- /// </summary>
- private async Task<bool> ActionPanicPunker(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Patch || context.Url.AbsolutePath != "/admin/actions/panic_bunker")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var body = await ReadJson<Dictionary<string, object>>(context);
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
- foreach (var panicPunkerActions in body!.Select(x => new { Action = x.Key, Value = x.Value.ToString() }))
- {
- if (panicPunkerActions.Action == null || panicPunkerActions.Value == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Action and value are required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "Action and value are required to perform this action.",
- ErrorType = ErrorTypes.ActionNotSpecified
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- if (!PanicPunkerCvarNames.TryGetValue(panicPunkerActions.Action, out var cvarName))
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = $"Cannot set: Action {panicPunkerActions.Action} does not exist.",
- Exception = new ExceptionData()
- {
- Message = $"Cannot set: Action {panicPunkerActions.Action} does not exist.",
- ErrorType = ErrorTypes.ActionNotSupported
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- // Since the CVar can be of different types, we need to parse it to the correct type
- // First, I try to parse it as a bool, if it fails, I try to parse it as an int
- // And as a last resort, I do nothing and put it as a string
- if (bool.TryParse(panicPunkerActions.Value, out var boolValue))
- {
- await RunOnMainThread(() => _config.SetCVar(cvarName, boolValue));
- }
- else if (int.TryParse(panicPunkerActions.Value, out var intValue))
- {
- await RunOnMainThread(() => _config.SetCVar(cvarName, intValue));
- }
- else
- {
- await RunOnMainThread(() => _config.SetCVar(cvarName, panicPunkerActions.Value));
- }
- _sawmill.Info($"Panic bunker property {panicPunkerActions} changed to {panicPunkerActions.Value} by {actor!.Name} ({actor.Guid}).");
- }
-
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- return true;
- }
-
- /// <summary>
- /// Sets the current MOTD.
- /// </summary>
- private async Task<bool> ActionForceMotd(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/set_motd")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var motd = await ReadJson<MotdActionBody>(context);
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
-
- if (motd!.Motd == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "A motd is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "A motd is required to perform this action.",
- ErrorType = ErrorTypes.MotdNotSpecified
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- _sawmill.Info($"MOTD changed to \"{motd.Motd}\" by {actor!.Name} ({actor.Guid}).");
-
- await RunOnMainThread(() => _config.SetCVar(CCVars.MOTD, motd.Motd));
- // A hook in the MOTD system sends the changes to each client
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- return true;
- }
-
- /// <summary>
- /// Forces the next preset-
- /// </summary>
- private async Task<bool> ActionForcePreset(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/force_preset")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var body = await ReadJson<PresetActionBody>(context);
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
- var ticker = await RunOnMainThread(() => _entitySystemManager.GetEntitySystem<GameTicker>());
-
- if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Round already started",
- Exception = new ExceptionData()
- {
- Message = "Round already started",
- ErrorType = ErrorTypes.RoundAlreadyStarted
- }
- }, HttpStatusCode.Conflict);
- return true;
- }
-
- if (body!.PresetId == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "A preset is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "A preset is required to perform this action.",
- ErrorType = ErrorTypes.PresetNotSpecified
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- var result = await RunOnMainThread(() => ticker.FindGamePreset(body.PresetId));
- if (result == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Preset not found",
- Exception = new ExceptionData()
- {
- Message = "Preset not found",
- ErrorType = ErrorTypes.PresetNotSpecified
- }
- }, HttpStatusCode.UnprocessableContent);
- return true;
- }
-
- await RunOnMainThread(() =>
- {
- ticker.SetGamePreset(result);
- });
- _sawmill.Info($"Forced the game to start with preset {body.PresetId} by {actor!.Name}({actor.Guid}).");
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- return true;
- }
-
- /// <summary>
- /// Ends an active game rule.
- /// </summary>
- private async Task<bool> ActionEndGameRule(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/end_game_rule")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var body = await ReadJson<GameRuleActionBody>(context);
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
- if (body!.GameRuleId == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "A game rule is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "A game rule is required to perform this action.",
- ErrorType = ErrorTypes.GuidNotSpecified
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
- var ticker = await RunOnMainThread(() => _entitySystemManager.GetEntitySystem<GameTicker>());
-
- var gameRuleEntity = await RunOnMainThread(() => ticker
- .GetActiveGameRules()
- .FirstOrNull(rule => _entityManager.MetaQuery.GetComponent(rule).EntityPrototype?.ID == body.GameRuleId));
-
- if (gameRuleEntity == null) // Game rule not found
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Game rule not found or not active",
- Exception = new ExceptionData()
- {
- Message = "Game rule not found or not active",
- ErrorType = ErrorTypes.GameRuleNotFound
- }
- }, HttpStatusCode.Conflict);
- return true;
- }
-
- _sawmill.Info($"Ended game rule {body.GameRuleId} by {actor!.Name} ({actor.Guid}).");
- await RunOnMainThread(() => ticker.EndGameRule((EntityUid) gameRuleEntity));
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- return true;
- }
-
- /// <summary>
- /// Adds a game rule to the current round.
- /// </summary>
- private async Task<bool> ActionAddGameRule(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/add_game_rule")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var body = await ReadJson<GameRuleActionBody>(context);
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
- if (body!.GameRuleId == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "A game rule is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "A game rule is required to perform this action.",
- ErrorType = ErrorTypes.GuidNotSpecified
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- var ruleEntity = await RunOnMainThread<EntityUid?>(() =>
- {
- var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
- // See if prototype exists
- try
- {
- _prototypeManager.Index(body.GameRuleId);
- }
- catch (KeyNotFoundException e)
- {
- return null;
- }
-
- var ruleEntity = ticker.AddGameRule(body.GameRuleId);
- _sawmill.Info($"Added game rule {body.GameRuleId} by {actor!.Name} ({actor.Guid}).");
- if (ticker.RunLevel == GameRunLevel.InRound)
- {
- ticker.StartGameRule(ruleEntity);
- _sawmill.Info($"Started game rule {body.GameRuleId} by {actor.Name} ({actor.Guid}).");
- }
- return ruleEntity;
- });
- if (ruleEntity == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Game rule not found",
- Exception = new ExceptionData()
- {
- Message = "Game rule not found",
- ErrorType = ErrorTypes.GameRuleNotFound
- }
- }, HttpStatusCode.UnprocessableContent);
- return true;
- }
-
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- return true;
- }
-
- /// <summary>
- /// Kicks a player.
- /// </summary>
- private async Task<bool> ActionKick(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/kick")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var body = await ReadJson<KickActionBody>(context);
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
- if (body == null)
- {
- _sawmill.Info($"Attempted to kick player without supplying a body by {actor!.Name}({actor.Guid}).");
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "A body is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "A body is required to perform this action.",
- ErrorType = ErrorTypes.BodyUnableToParse
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- if (body.Guid == null)
- {
- _sawmill.Info($"Attempted to kick player without supplying a username by {actor!.Name}({actor.Guid}).");
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "A player is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "A player is required to perform this action.",
- ErrorType = ErrorTypes.GuidNotSpecified
- }
- }, HttpStatusCode.BadRequest);
- return true;
- }
-
- var session = await RunOnMainThread(() =>
- {
- _playerManager.TryGetSessionById(new NetUserId(new Guid(body.Guid)), out var player);
- return player;
- });
-
- if (session == null)
- {
- _sawmill.Info($"Attempted to kick player {body.Guid} by {actor!.Name} ({actor.Guid}), but they were not found.");
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Player not found",
- Exception = new ExceptionData()
- {
- Message = "Player not found",
- ErrorType = ErrorTypes.PlayerNotFound
- }
- }, HttpStatusCode.UnprocessableContent);
- return true;
- }
-
- var reason = body.Reason ?? "No reason supplied";
- reason += " (kicked by admin)";
-
- await RunOnMainThread(() =>
- {
- _netManager.DisconnectChannel(session.Channel, reason);
- });
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- _sawmill.Info("Kicked player {0} ({1}) for {2} by {3}({4})", session.Name, session.UserId.UserId.ToString(), reason, actor!.Name, actor.Guid);
- return true;
- }
-
- /// <summary>
- /// Round restart/end
- /// </summary>
- private async Task<bool> ActionRoundStatus(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Post || !context.Url.AbsolutePath.StartsWith("/admin/actions/round/"))
- {
- return false;
- }
-
- // Make sure paths like /admin/actions/round/lol/start don't work
- if (context.Url.AbsolutePath.Split('/').Length != 5)
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
-
- var (ticker, roundEndSystem) = await RunOnMainThread(() =>
- {
- var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
- var roundEndSystem = _entitySystemManager.GetEntitySystem<RoundEndSystem>();
- return (ticker, roundEndSystem);
- });
-
- // Action is the last part of the URL path (e.g. /admin/actions/round/start -> start)
- var action = context.Url.AbsolutePath.Split('/').Last();
-
- switch (action)
- {
- case "start":
- if (ticker.RunLevel != GameRunLevel.PreRoundLobby)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Round already started",
- Exception = new ExceptionData()
- {
- Message = "Round already started",
- ErrorType = ErrorTypes.RoundAlreadyStarted
- }
- }, HttpStatusCode.Conflict);
- _sawmill.Debug("Forced round start failed: round already started");
- return true;
- }
-
- await RunOnMainThread(() =>
- {
- ticker.StartRound();
- });
- _sawmill.Info("Forced round start");
- break;
- case "end":
- if (ticker.RunLevel != GameRunLevel.InRound)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Round already ended",
- Exception = new ExceptionData()
- {
- Message = "Round already ended",
- ErrorType = ErrorTypes.RoundAlreadyEnded
- }
- }, HttpStatusCode.Conflict);
- _sawmill.Debug("Forced round end failed: round is not in progress");
- return true;
- }
- await RunOnMainThread(() =>
- {
- roundEndSystem.EndRound();
- });
- _sawmill.Info("Forced round end");
- break;
- case "restart":
- if (ticker.RunLevel != GameRunLevel.InRound)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Round not in progress",
- Exception = new ExceptionData()
- {
- Message = "Round not in progress",
- ErrorType = ErrorTypes.RoundNotInProgress
- }
- }, HttpStatusCode.Conflict);
- _sawmill.Debug("Forced round restart failed: round is not in progress");
- return true;
- }
- await RunOnMainThread(() =>
- {
- roundEndSystem.EndRound();
- });
- _sawmill.Info("Forced round restart");
- break;
- case "restartnow": // You should restart yourself NOW!!!
- await RunOnMainThread(() =>
- {
- ticker.RestartRound();
- });
- _sawmill.Info("Forced instant round restart");
- break;
- default:
- return false;
- }
-
- _sawmill.Info($"Round {action} by {actor!.Name} ({actor.Guid}).");
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "OK"
- });
- return true;
- }
-#endregion
-
-#region Fetching
-
- /// <summary>
- /// Returns an array containing all available presets.
- /// </summary>
- private async Task<bool> GetForcePresets(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/force_presets")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var presets = new List<(string id, string desc)>();
- foreach (var preset in _prototypeManager.EnumeratePrototypes<GamePresetPrototype>())
- {
- presets.Add((preset.ID, preset.Description));
- }
-
- await context.RespondJsonAsync(new PresetResponse()
- {
- Presets = presets
- });
- return true;
- }
-
- /// <summary>
- /// Returns an array containing all game rules.
- /// </summary>
- private async Task<bool> GetGameRules(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/game_rules")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var gameRules = new List<string>();
- foreach (var gameRule in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
- {
- if (gameRule.Abstract)
- continue;
-
- if (gameRule.HasComponent<GameRuleComponent>(_componentFactory))
- gameRules.Add(gameRule.ID);
- }
-
- await context.RespondJsonAsync(new GameruleResponse()
- {
- GameRules = gameRules
- });
- return true;
- }
-
-
- /// <summary>
- /// Handles fetching information.
- /// </summary>
- private async Task<bool> InfoHandler(IStatusHandlerContext context)
- {
- if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/info")
- {
- return false;
- }
-
- if (!CheckAccess(context))
- return true;
-
- var (success, actor) = await CheckActor(context);
- if (!success)
- return true;
-
- /* Information to display
- Round number
- Connected players
- Active admins
- Active game rules
- Active game preset
- Active map
- MOTD
- Panic bunker status
- */
-
- var (ticker, adminSystem) = await RunOnMainThread(() =>
- {
- var ticker = _entitySystemManager.GetEntitySystem<GameTicker>();
- var adminSystem = _entitySystemManager.GetEntitySystem<AdminSystem>();
- return (ticker, adminSystem);
- });
-
- var players = new List<Actor>();
- await RunOnMainThread(async () =>
- {
- foreach (var player in _playerManager.Sessions)
- {
- var isAdmin = _adminManager.IsAdmin(player);
- var isDeadmined = _adminManager.IsAdmin(player, true) && !isAdmin;
-
- players.Add(new Actor()
- {
- Guid = player.UserId.UserId.ToString(),
- Name = player.Name,
- IsAdmin = isAdmin,
- IsDeadmined = isDeadmined
- });
- }
- });
- var gameRules = await RunOnMainThread(() =>
- {
- var gameRules = new List<string>();
- foreach (var addedGameRule in ticker.GetActiveGameRules())
- {
- var meta = _entityManager.MetaQuery.GetComponent(addedGameRule);
- gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown");
- }
-
- return gameRules;
- });
-
- _sawmill.Info($"Info requested by {actor!.Name} ({actor.Guid}).");
- await context.RespondJsonAsync(new InfoResponse()
- {
- Players = players,
- RoundId = ticker.RoundId,
- Map = await RunOnMainThread(() => _gameMapManager.GetSelectedMap()?.MapName ?? "Unknown"),
- PanicBunker = adminSystem.PanicBunker,
- GamePreset = ticker.CurrentPreset?.ID,
- GameRules = gameRules,
- MOTD = _config.GetCVar(CCVars.MOTD)
- });
- return true;
- }
-
-#endregion
-
- private bool CheckAccess(IStatusHandlerContext context)
- {
- var auth = context.RequestHeaders.TryGetValue("Authorization", out var authToken);
- if (!auth)
- {
- context.RespondJsonAsync(new BaseResponse()
- {
- Message = "An authorization header is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "An authorization header is required to perform this action.",
- ErrorType = ErrorTypes.MissingAuthentication
- }
- });
- return false;
- }
-
-
- if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(authToken.ToString()), Encoding.UTF8.GetBytes(_token)))
- return true;
-
- context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Invalid authorization header.",
- Exception = new ExceptionData()
- {
- Message = "Invalid authorization header.",
- ErrorType = ErrorTypes.InvalidAuthentication
- }
- });
- // Invalid auth header, no access
- _sawmill.Info("Unauthorized access attempt to admin API.");
- return false;
- }
-
- /// <summary>
- /// Async helper function which runs a task on the main thread and returns the result.
- /// </summary>
- private async Task<T> RunOnMainThread<T>(Func<T> func)
- {
- var taskCompletionSource = new TaskCompletionSource<T>();
- _taskManager.RunOnMainThread(() =>
- {
- try
- {
- taskCompletionSource.TrySetResult(func());
- }
- catch (Exception e)
- {
- taskCompletionSource.TrySetException(e);
- }
- });
-
- var result = await taskCompletionSource.Task;
- return result;
- }
-
- /// <summary>
- /// Runs an action on the main thread. This does not return any value and is meant to be used for void functions. Use <see cref="RunOnMainThread{T}"/> for functions that return a value.
- /// </summary>
- private async Task RunOnMainThread(Action action)
- {
- var taskCompletionSource = new TaskCompletionSource<bool>();
- _taskManager.RunOnMainThread(() =>
- {
- try
- {
- action();
- taskCompletionSource.TrySetResult(true);
- }
- catch (Exception e)
- {
- taskCompletionSource.TrySetException(e);
- }
- });
-
- await taskCompletionSource.Task;
- }
-
- private async Task<(bool, Actor? actor)> CheckActor(IStatusHandlerContext context)
- {
- // The actor is JSON encoded in the header
- var actor = context.RequestHeaders.TryGetValue("Actor", out var actorHeader) ? actorHeader.ToString() : null;
- if (actor != null)
- {
- var actionData = JsonSerializer.Deserialize<Actor>(actor);
- if (actionData == null)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Unable to parse actor.",
- Exception = new ExceptionData()
- {
- Message = "Unable to parse actor.",
- ErrorType = ErrorTypes.BodyUnableToParse
- }
- }, HttpStatusCode.BadRequest);
- return (false, null);
- }
- // Check if the actor is valid, like if all the required fields are present
- if (string.IsNullOrWhiteSpace(actionData.Guid) || string.IsNullOrWhiteSpace(actionData.Name))
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Invalid actor supplied.",
- Exception = new ExceptionData()
- {
- Message = "Invalid actor supplied.",
- ErrorType = ErrorTypes.InvalidActor
- }
- }, HttpStatusCode.BadRequest);
- return (false, null);
- }
-
- // See if the parsed GUID is a valid GUID
- if (!Guid.TryParse(actionData.Guid, out _))
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Invalid GUID supplied.",
- Exception = new ExceptionData()
- {
- Message = "Invalid GUID supplied.",
- ErrorType = ErrorTypes.InvalidActor
- }
- }, HttpStatusCode.BadRequest);
- return (false, null);
- }
-
- return (true, actionData);
- }
-
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "An actor is required to perform this action.",
- Exception = new ExceptionData()
- {
- Message = "An actor is required to perform this action.",
- ErrorType = ErrorTypes.MissingActor
- }
- }, HttpStatusCode.BadRequest);
- return (false, null);
- }
-
- /// <summary>
- /// Helper function to read JSON encoded data from the request body.
- /// </summary>
- private async Task<T?> ReadJson<T>(IStatusHandlerContext context)
- {
- try
- {
- var json = await context.RequestBodyJsonAsync<T>();
- return json;
- }
- catch (Exception e)
- {
- await context.RespondJsonAsync(new BaseResponse()
- {
- Message = "Unable to parse request body.",
- Exception = new ExceptionData()
- {
- Message = e.Message,
- ErrorType = ErrorTypes.BodyUnableToParse,
- StackTrace = e.StackTrace
- }
- }, HttpStatusCode.BadRequest);
- return default;
- }
- }
-
-#region From Client
-
- private record Actor
- {
- public string? Guid { get; init; }
- public string? Name { get; init; }
- public bool IsAdmin { get; init; } = false;
- public bool IsDeadmined { get; init; } = false;
- }
-
- private record KickActionBody
- {
- public string? Guid { get; init; }
- public string? Reason { get; init; }
- }
-
- private record GameRuleActionBody
- {
- public string? GameRuleId { get; init; }
- }
-
- private record PresetActionBody
- {
- public string? PresetId { get; init; }
- }
-
- private record MotdActionBody
- {
- public string? Motd { get; init; }
- }
-
-#endregion
-
-#region Responses
-
- private record BaseResponse
- {
- public string? Message { get; init; } = "OK";
- public ExceptionData? Exception { get; init; } = null;
- }
-
- private record ExceptionData
- {
- public string Message { get; init; } = string.Empty;
- public ErrorTypes ErrorType { get; init; } = ErrorTypes.None;
- public string? StackTrace { get; init; } = null;
- }
-
- private enum ErrorTypes
- {
- BodyUnableToParse = -2,
- None = -1,
- MissingAuthentication = 0,
- InvalidAuthentication = 1,
- MissingActor = 2,
- InvalidActor = 3,
- RoundNotInProgress = 4,
- RoundAlreadyStarted = 5,
- RoundAlreadyEnded = 6,
- ActionNotSpecified = 7,
- ActionNotSupported = 8,
- GuidNotSpecified = 9,
- PlayerNotFound = 10,
- GameRuleNotFound = 11,
- PresetNotSpecified = 12,
- MotdNotSpecified = 13
- }
-
-#endregion
-
-#region Misc
-
- /// <summary>
- /// Record used to send the response for the info endpoint.
- /// </summary>
- private record InfoResponse
- {
- public int RoundId { get; init; } = 0;
- public List<Actor> Players { get; init; } = new();
- public List<string> GameRules { get; init; } = new();
- public string? GamePreset { get; init; } = null;
- public string? Map { get; init; } = null;
- public string? MOTD { get; init; } = null;
- public PanicBunkerStatus PanicBunker { get; init; } = new();
- }
-
- private record PresetResponse : BaseResponse
- {
- public List<(string id, string desc)> Presets { get; init; } = new();
- }
-
- private record GameruleResponse : BaseResponse
- {
- public List<string> GameRules { get; init; } = new();
- }
-
-#endregion
-
-}