Files
Radicade/Server/Config/PlayerSettingsAPI.cs
T
2026-10-16 23:33:13 -07:00

355 lines
11 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Accounts.Local;
namespace RadicadeServer.API.Config
{
[Route("api/settings")]
[Route("playersettings")]
public class PlayerSettingsAPI : Controller
{
private readonly RecDBContext db;
public PlayerSettingsAPI(RecDBContext context)
{
db = context;
}
[HttpGet(""), Authorize]
[HttpGet("v2"), Authorize]
[HttpGet("playersettings"), Authorize]
public List<Setting> GetSettings()
{
var userId = User.GetUserId();
if (userId == null)
return new List<Setting>();
var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value);
var list = settings?.DeserializeSettings() ?? GetDefaultSettings();
ApplyDefaultSettings(list);
var player = db.Players.AsNoTracking().FirstOrDefault(x => x.AccountId == userId.Value);
if (player != null && string.IsNullOrWhiteSpace(player.Hashword))
{
SetOrReplace(list, "Recroom.AccountCreation.HasCreatedPassword", "False");
SetOrReplace(list, "Recroom.AccountCreation.HasFinished", "False");
SetOrReplace(list, "Recroom.OOBE", "0");
}
ApplyResponseMetadata(list, userId.Value);
if (settings == null)
{
db.PlayerSettings.Add(new PlayerSettings
{
AccountId = userId.Value,
Settings = list.ToJson()
});
db.SaveChanges();
}
return list;
}
private static void SetOrReplace(List<Setting> list, string key, string value)
{
var existing = list.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase));
if (existing != null)
existing.Value = value;
else
list.Add(new Setting { Key = key, Value = value });
}
private static void ApplyDefaultSettings(List<Setting> list)
{
bool needsOrientation = list.Any(setting =>
string.Equals(setting.Key, "Radicade.NeedsOrientation", StringComparison.OrdinalIgnoreCase) &&
(setting.Value == "1" || string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)));
foreach (var setting in GetDefaultSettings())
{
if (needsOrientation &&
(string.Equals(setting.Key, "HAS_COMPLETED_ORIENTATION", StringComparison.OrdinalIgnoreCase) ||
string.Equals(setting.Key, "OrientationCompletionTime", StringComparison.OrdinalIgnoreCase)))
continue;
if (list.All(existing => !string.Equals(existing.Key, setting.Key, StringComparison.OrdinalIgnoreCase)))
list.Add(setting);
}
SetOrReplace(list, "SplitTestAssignedSegments", "1|{}");
}
private static void ApplyResponseMetadata(List<Setting> list, long accountId)
{
for (var i = 0; i < list.Count; i++)
{
list[i].Id = i + 1;
list[i].AccountId = accountId;
}
}
public const int InviteLinkLifetimeInMinutes = 60;
[HttpGet("partyinvite")]
[HttpGet("~/chat/settings/partyinvite")]
public IActionResult GetPartyInviteSettings() => Ok(new { InviteLinkLifetimeInMinutes });
[HttpPost("v2/set"), Authorize]
[HttpPost(""), Authorize]
[HttpPost("playersettings/set"), Authorize]
public async Task<IActionResult> SetSettingRequest()
{
if (User.GetUserId() == null) return Unauthorized();
var body = await ReadJsonBodyAsync();
var updates = body.HasValue ? ReadSettings(body.Value) : new List<Setting>();
if (updates.Count != 1) return BadRequest("Expected one setting with Key and Value.");
return await SetSetting(updates[0]);
}
[NonAction]
public async Task<IActionResult> SetSetting(Setting val)
{
var userId = User.GetUserId();
if (userId == null)
return Unauthorized();
if (val == null || string.IsNullOrWhiteSpace(val.Key) || val.Value == null)
return BadRequest("Expected a setting with Key and Value.");
var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value);
if (settings == null)
{
settings = new PlayerSettings
{
AccountId = userId.Value,
Settings = DBConfig.DefaultSettings2023
};
await db.PlayerSettings.AddAsync(settings);
}
var actualSettings = settings.DeserializeSettings();
if (!await CanSaveOnboardingMarkerAsync(userId.Value, val, actualSettings, new[] { val }))
return Ok(Array.Empty<object>());
SetOrReplace(actualSettings, val.Key, val.Value);
if (IsOnboardingFinishedMarker(val))
SetOrReplace(actualSettings, "Radicade.NeedsOrientation", "1");
settings.Settings = actualSettings.ToJson();
await db.SaveChangesAsync();
await RecordCodeOfConductAsync(userId.Value, new[] { val }, actualSettings);
return Ok(Array.Empty<object>());
}
[HttpPut(""), Authorize]
[HttpPut("v2"), Authorize]
[HttpPut("playersettings"), Authorize]
public async Task<IActionResult> PutSettings()
{
var userId = User.GetUserId();
if (userId == null)
return Unauthorized();
var body = await ReadJsonBodyAsync();
var updates = body.HasValue ? ReadSettings(body.Value) : new List<Setting>();
if (updates.Count == 0)
return BadRequest("No valid settings were supplied.");
var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value);
if (settings == null)
{
settings = new PlayerSettings
{
AccountId = userId.Value,
Settings = DBConfig.DefaultSettings2023
};
await db.PlayerSettings.AddAsync(settings);
}
var actualSettings = settings.DeserializeSettings();
foreach (var update in updates)
{
if (string.IsNullOrWhiteSpace(update.Key) || update.Value == null ||
!await CanSaveOnboardingMarkerAsync(userId.Value, update, actualSettings, updates))
continue;
SetOrReplace(actualSettings, update.Key, update.Value);
if (IsOnboardingFinishedMarker(update))
SetOrReplace(actualSettings, "Radicade.NeedsOrientation", "1");
}
settings.Settings = actualSettings.ToJson();
await db.SaveChangesAsync();
await RecordCodeOfConductAsync(userId.Value, updates, actualSettings);
ApplyResponseMetadata(actualSettings, userId.Value);
return Ok(actualSettings);
}
private async Task RecordCodeOfConductAsync(
long accountId,
IEnumerable<Setting> incomingSettings,
IEnumerable<Setting> savedSettings)
{
bool accepted = incomingSettings.Concat(savedSettings).Any(setting =>
string.Equals(setting.Key, "Recroom.AccountCreation.AcceptedCodeOfConduct", StringComparison.OrdinalIgnoreCase) &&
string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase));
if (!accepted) return;
var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == accountId);
if (player == null || player.CodeOfConductAccepted) return;
player.CodeOfConductAccepted = true;
await db.SaveChangesAsync();
}
private static bool IsOnboardingFinishedMarker(Setting setting) =>
(string.Equals(setting.Key, "Recroom.OOBE", StringComparison.OrdinalIgnoreCase) && setting.Value == "100") ||
(string.Equals(setting.Key, "Recroom.AccountCreation.HasFinished", StringComparison.OrdinalIgnoreCase) &&
string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase));
private async Task<bool> CanSaveOnboardingMarkerAsync(
long accountId,
Setting update,
IReadOnlyCollection<Setting> savedSettings,
IReadOnlyCollection<Setting> incomingSettings)
{
bool marksPasswordCreated =
string.Equals(update.Key, "Recroom.AccountCreation.HasCreatedPassword", StringComparison.OrdinalIgnoreCase) &&
string.Equals(update.Value, "true", StringComparison.OrdinalIgnoreCase);
if (!marksPasswordCreated && !IsOnboardingFinishedMarker(update))
return true;
var player = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId);
if (player == null || string.IsNullOrWhiteSpace(player.Hashword))
{
return false;
}
if (marksPasswordCreated)
return true;
var allSettings = incomingSettings.Concat(savedSettings).ToList();
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId);
bool hasBirthday = player.Birthday.HasValue && player.Birthday.Value.Year > 1900;
bool hasChosenUsername = allSettings.Any(setting =>
string.Equals(setting.Key, "Recroom.AccountCreation.HasChosenUsername", StringComparison.OrdinalIgnoreCase) &&
string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase));
bool hasAcceptedCodeOfConduct = player.CodeOfConductAccepted || allSettings.Any(setting =>
string.Equals(setting.Key, "Recroom.AccountCreation.AcceptedCodeOfConduct", StringComparison.OrdinalIgnoreCase) &&
string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase));
if (profile == null || string.IsNullOrWhiteSpace(profile.username))
{
return false;
}
if (!hasChosenUsername)
{
return false;
}
if (!hasAcceptedCodeOfConduct)
{
return false;
}
return true;
}
private static List<Setting> GetDefaultSettings()
{
try
{
return DBConfig.DefaultSettings2023.FromJson<List<Setting>>() ?? new List<Setting>();
}
catch (Exception)
{
return new List<Setting>();
}
}
private async Task<JsonElement?> ReadJsonBodyAsync()
{
if (Request.Body == null)
return null;
if (Request.ContentLength == 0)
return null;
try
{
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
return JsonSerializer.SerializeToElement(form.ToDictionary(pair => pair.Key, pair => pair.Value.ToString()));
}
using var doc = await JsonDocument.ParseAsync(Request.Body);
return doc.RootElement.Clone();
}
catch (JsonException)
{
return null;
}
}
private static List<Setting> ReadSettings(JsonElement body)
{
if (body.ValueKind == JsonValueKind.Array)
return ReadSettingsArray(body);
if (body.ValueKind != JsonValueKind.Object)
return new List<Setting>();
foreach (var key in new[] { "settings", "Settings", "playerSettings", "PlayerSettings" })
{
if (body.TryGetProperty(key, out var nested) && nested.ValueKind == JsonValueKind.Array)
return ReadSettingsArray(nested);
}
var setting = ReadSettingObject(body);
return setting == null ? new List<Setting>() : new List<Setting> { setting };
}
private static List<Setting> ReadSettingsArray(JsonElement array)
{
var settings = new List<Setting>();
foreach (var item in array.EnumerateArray())
{
var setting = ReadSettingObject(item);
if (setting != null)
settings.Add(setting);
}
return settings;
}
private static Setting? ReadSettingObject(JsonElement item)
{
if (item.ValueKind != JsonValueKind.Object)
return null;
var key = ReadString(item, "Key", "key", "Name", "name");
var value = ReadString(item, "Value", "value");
if (string.IsNullOrWhiteSpace(key) || value == null)
return null;
return new Setting { Key = key, Value = value };
}
private static string? ReadString(JsonElement item, params string[] names)
{
foreach (var name in names)
{
if (!item.TryGetProperty(name, out var value))
continue;
return value.ValueKind switch
{
JsonValueKind.String => value.GetString(),
JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.ToString(),
_ => null,
};
}
return null;
}
}
}