:3
This commit is contained in:
@@ -0,0 +1,818 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Mocha2023.Classes;
|
||||
using Mocha2023.Classes.DBs;
|
||||
using static Mocha2023.Classes.DBs.PlayerDB;
|
||||
using Mocha2023.Classes.DBs.DBClasses;
|
||||
using Mocha2023.Auth;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Mocha2023.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Mocha2023.Classes.ApiProtection]
|
||||
[Route("/acc")]
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
[HttpGet("account/bulk")]
|
||||
public IActionResult GetAccountsBulk([FromQuery] List<long> id)
|
||||
{
|
||||
var authId = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
var accounts = PlayerDB.GetAccountsBulk(id, authId);
|
||||
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
[HttpPut("account/me/displayname")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> UpdateDisplayName([FromForm] string displayName)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
displayName = displayName?.Trim() ?? string.Empty;
|
||||
if (displayName.Length is < 1 or > 32 || displayName.Any(char.IsControl))
|
||||
return BadRequest(new { error = "invalid_display_name" });
|
||||
|
||||
if (!PlayerDB.UpdateDisplayName(id.Value, displayName))
|
||||
{
|
||||
DiscordLogger.Log(
|
||||
$"? **Display Name Change Failed**\n**Player ID:** `{id.Value}`\n**Attempted:** `{displayName}`"
|
||||
);
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
DiscordLogger.Log(
|
||||
$"?? **Display Name Changed**\n**Player ID:** `{id.Value}`\n**New Display Name:** `{displayName}`"
|
||||
);
|
||||
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(id.Value);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPut("/acc/account/me/emoji")]
|
||||
[Consumes("application/x-www-form-urlencoded")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> UpdateEmoji(
|
||||
[FromForm] string? displayEmoji)
|
||||
{
|
||||
long? accountId = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
if (!accountId.HasValue)
|
||||
return Unauthorized();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(displayEmoji))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = "displayEmoji is required"
|
||||
});
|
||||
}
|
||||
|
||||
if (displayEmoji.Length > 16 || displayEmoji.Any(char.IsControl))
|
||||
return BadRequest(new { success = false, error = "invalid_displayEmoji" });
|
||||
|
||||
bool updated = PlayerDB.UpdateDisplayEmoji(
|
||||
accountId.Value,
|
||||
displayEmoji);
|
||||
|
||||
if (!updated)
|
||||
return NotFound();
|
||||
|
||||
Console.WriteLine(
|
||||
$"[EMOJI] Player {accountId.Value} selected {displayEmoji}");
|
||||
|
||||
var account = PlayerDB.GetAccountMe(accountId.Value);
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(accountId.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
account
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/acc/emojiConfig/whitelistedEmojis")]
|
||||
public IActionResult GetWhitelistedEmojis()
|
||||
{
|
||||
string[] emojis =
|
||||
{
|
||||
"😀","😃","😄","😁","😆","😅","😂","🤣","😊","😇",
|
||||
"🙂","🙃","😉","😍","🥰","😘","😗","😚","😋","😛",
|
||||
"😜","🤪","😝","🤑","🤗","🤭","🤫","🤔","🤨","😐",
|
||||
"😑","😶","🙄","😏","😣","😥","😮","🤐","😯","😪",
|
||||
"😫","🥱","😴","😌","🤓","😎","🤩","🥳","😤","😭",
|
||||
"😢","🥺","😡","🤬","😱","😨","😰","😬","🤯","🥶",
|
||||
"🥵","🤠","🤖","👽","👻","💀","☠️","👹","👺","👾",
|
||||
"❤️","🧡","💛","💚","💙","💜","🖤","🤍","🤎",
|
||||
"💖","💗","💓","💕","💞","💘","💝","💟","❣️","💔",
|
||||
"👍","👎","👌","✌️","🤞","🤟","🤘","🤙",
|
||||
"👏","🙌","👐","🤲","🙏","👋","✋","🤚","🫶",
|
||||
"💪","🧠","👀","👁️","👄","🦾","🦿",
|
||||
"🐶","🐱","🐭","🐹","🐰","🦊","🐻","🐼","🐨","🐯",
|
||||
"🦁","🐸","🐵","🐧","🐦","🦅","🦆","🦄","🐴","🐢",
|
||||
"🐙","🦈","🐬","🐳","🦋","🐝","🐞","🦖","🦕","🐲",
|
||||
"🍎","🍌","🍇","🍉","🍓","🍒","🥝","🍍","🥑","🌮",
|
||||
"🍕","🍔","🍟","🌭","🥪","🍗","🍿","🍩","🍪","🎂",
|
||||
"🍫","🍬","🍭","🧋","☕","🥤","🍺","🥛",
|
||||
"☀️","🌤️","⛅","🌥️","☁️","🌧️","⛈️","❄️","🌈",
|
||||
"⭐","🌟","✨","⚡","🔥","💧","🌊","🌸","🌹","🍀","🌲",
|
||||
"🎮","🕹️","👾","💻","⌨️","🖥️","📱","🖱️","🎧","📷",
|
||||
"📹","💿","💾","🔋","🔌","🛰️","🚀",
|
||||
"🎵","🎶","🎼","🎤","🎧","🥁","🎸","🎹","🎺","🎷","🎻",
|
||||
"⚽","🏀","🏈","⚾","🎾","🏐","🏓","🥊","🏆","🥇","🥈","🥉",
|
||||
"💎","💰","💸","🪙","🎁","📦","🔑","🗝️","🛡️","⚔️",
|
||||
"🧸","🎈","🎉","🎊","🕯️","💡","📚","📖","✏️","🖊️",
|
||||
"🚗","🚕","🚌","🚓","🚑","🚒","🏎️","🚲","✈️","🚁","🚢",
|
||||
"✔️","✅","❌","⭕","❗","❓","💯","♾️","🔔","🔕","❤️🔥"
|
||||
};
|
||||
|
||||
Console.WriteLine(
|
||||
$"[EMOJI] Whitelist requested; returning {emojis.Length} emojis."
|
||||
);
|
||||
|
||||
return Ok(emojis);
|
||||
}
|
||||
|
||||
[HttpPut("account/me/bio")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public IActionResult UpdateMyBio([FromForm] string? bio)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player?.Player == null)
|
||||
return NotFound();
|
||||
|
||||
bio = bio?.Trim() ?? string.Empty;
|
||||
if (bio.Length > 500 || bio.Any(ch => ch == '\0'))
|
||||
return BadRequest(new { error = "invalid_bio" });
|
||||
|
||||
player.Player.Bio = bio;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPut("account/me/bannerimage")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> UpdateBannerImage()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
var account = PlayerDB.Players.FindById(id.Value);
|
||||
if (account?.Player == null)
|
||||
return NotFound();
|
||||
|
||||
string? value = await ReadRequestValueAsync(
|
||||
"bannerImage",
|
||||
"bannerImageName",
|
||||
"imageName",
|
||||
"ImageName",
|
||||
"filename",
|
||||
"Filename",
|
||||
"name",
|
||||
"value");
|
||||
string? requested = NormalizeProfileImagePath(
|
||||
value,
|
||||
id.Value,
|
||||
requireExistingFile: false);
|
||||
if (requested == null)
|
||||
return BadRequest(new { success = false, error = "invalid_banner_image" });
|
||||
|
||||
account.Player.BannerImage = requested;
|
||||
PlayerDB.Players.Update(account);
|
||||
Console.WriteLine($"[BANNER IMAGE] Player {id.Value} set to {requested}");
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(id.Value);
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
bannerImage = account.Player.BannerImage,
|
||||
url = $"{ServerConfig.BaseURL}/imageserver/{account.Player.BannerImage}"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("namegen/options")]
|
||||
public IActionResult GetNameGenOptions()
|
||||
{
|
||||
var options = Enumerable.Range(0, 5)
|
||||
.Select(_ => Classes.NameGen.GetRandomName())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
Console.WriteLine($"[NAMEGEN] Returning options: {string.Join(", ", options)}");
|
||||
|
||||
return Ok(options);
|
||||
}
|
||||
|
||||
[HttpPut("account/me/profileimage")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> UpdateProfileImage()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
var account = PlayerDB.Players.FindById(id.Value);
|
||||
if (account?.Player == null)
|
||||
return NotFound();
|
||||
|
||||
string? value = await ReadRequestValueAsync(
|
||||
"profileImage",
|
||||
"profileImageName",
|
||||
"imageName",
|
||||
"ImageName",
|
||||
"value");
|
||||
string? requested = NormalizeProfileImagePath(value, id.Value);
|
||||
if (requested == null)
|
||||
return BadRequest(new { success = false, error = "invalid_profile_image" });
|
||||
|
||||
account.Player.ProfileImage = requested;
|
||||
PlayerDB.Players.Update(account);
|
||||
|
||||
Console.WriteLine($"[PROFILE IMAGE] Player {id.Value} set to {requested}");
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(id.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
profileImage = account.Player.ProfileImage,
|
||||
url = $"{ServerConfig.BaseURL}/imageserver/{account.Player.ProfileImage}"
|
||||
});
|
||||
}
|
||||
|
||||
private static string? NormalizeProfileImagePath(
|
||||
string? value,
|
||||
long? ownerId = null,
|
||||
bool requireExistingFile = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return null;
|
||||
|
||||
string requested = value.Trim().Replace('\\', '/');
|
||||
if (Uri.TryCreate(requested, UriKind.Absolute, out var uri))
|
||||
{
|
||||
const string marker = "/imageserver/";
|
||||
int markerIndex = uri.AbsolutePath.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
|
||||
if (markerIndex < 0)
|
||||
return null;
|
||||
requested = Uri.UnescapeDataString(uri.AbsolutePath[(markerIndex + marker.Length)..]);
|
||||
}
|
||||
|
||||
requested = requested.TrimStart('/');
|
||||
string[] segments = requested.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (requested.Length > 260 || segments.Length == 0 ||
|
||||
requested.Any(char.IsControl) || requested.Contains(':') ||
|
||||
Path.IsPathRooted(requested) ||
|
||||
segments.Any(segment => segment is "." or "..") ||
|
||||
!new[] { ".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp" }
|
||||
.Contains(Path.GetExtension(requested), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
requested = string.Join('/', segments);
|
||||
if (!requested.Contains('/'))
|
||||
{
|
||||
string playerImage = Path.Combine(Program.dataDir, "Images", "PlayerImages", requested);
|
||||
if (System.IO.File.Exists(playerImage))
|
||||
{
|
||||
requested = $"PlayerImages/{requested}";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var savedImage = RecNetDB.SavedImages.FindAll()
|
||||
.Where(image => !ownerId.HasValue || image.AccountId == ownerId.Value)
|
||||
.OrderByDescending(image => image.CreatedAt)
|
||||
.FirstOrDefault(image => string.Equals(
|
||||
Path.GetFileName(image.PhotoPath),
|
||||
requested,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
if (savedImage != null)
|
||||
requested = savedImage.PhotoPath.Replace('\\', '/').TrimStart('/');
|
||||
}
|
||||
}
|
||||
|
||||
string imageRoot = Path.GetFullPath(Path.Combine(Program.dataDir, "Images"));
|
||||
string fullPath = Path.GetFullPath(Path.Combine(
|
||||
imageRoot,
|
||||
requested.Replace('/', Path.DirectorySeparatorChar)));
|
||||
if (!fullPath.StartsWith(
|
||||
imageRoot + Path.DirectorySeparatorChar,
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
(requireExistingFile && !System.IO.File.Exists(fullPath)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return requested;
|
||||
}
|
||||
|
||||
[HttpGet("accountprivacysettings/{accountId}")]
|
||||
public IActionResult GetAccountPrivacySettings(long accountId)
|
||||
{
|
||||
var callerId = AuthStuff.GetPlayerId(Request);
|
||||
bool isOwner = callerId.HasValue && callerId.Value == accountId;
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
accountId,
|
||||
isOwner,
|
||||
profileVisibility = 0,
|
||||
allowFriendRequests = true,
|
||||
allowPartyInvites = true,
|
||||
allowGifts = true,
|
||||
showOnlineStatus = true
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("account/{accountId}/bio")]
|
||||
public IActionResult GetAccountBio(long accountId)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(accountId);
|
||||
string bio = player?.Player?.Bio ?? "";
|
||||
|
||||
return Ok(new { success = true, bio });
|
||||
}
|
||||
|
||||
[HttpPut("account/me/username")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> UpdateUsername()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
string? username = await ReadRequestValueAsync("username");
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
return BadRequest(new { error = "username_required", message = "Username cannot be empty" });
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
bool initialAccountSetup = player?.Player?.PlayerExtra?.Settings?.Any(setting =>
|
||||
string.Equals(
|
||||
setting.Key,
|
||||
"Recroom.AccountCreation.HasChosenUsername",
|
||||
StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(setting.Value, "True", StringComparison.OrdinalIgnoreCase)) == true;
|
||||
|
||||
if (!PlayerDB.UpdateUsername(id.Value, username, initialAccountSetup))
|
||||
{
|
||||
DiscordLogger.Log(
|
||||
$"? **Username Change Failed**\n**Player ID:** `{id.Value}`\n**Attempted:** `{username}`"
|
||||
);
|
||||
return BadRequest(new { error = "username_change_failed", message = "No username changes remaining or invalid name" });
|
||||
}
|
||||
|
||||
DiscordLogger.Log(
|
||||
$"?? **Username Changed**\n**Player ID:** `{id.Value}`\n**New Username:** `{username}`"
|
||||
);
|
||||
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(id.Value);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPut("account/me/birthday")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> UpdateBirthday()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
string? birthdayText = await ReadRequestValueAsync("birthday", "dateOfBirth");
|
||||
if (!DateTime.TryParse(
|
||||
birthdayText,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal,
|
||||
out DateTime birthday) ||
|
||||
!PlayerDB.UpdateBirthday(id.Value, birthday))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "invalid_birthday",
|
||||
message = "Enter a valid birthday."
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPost("account/me/changepassword")]
|
||||
[HttpPut("account/me/changepassword")]
|
||||
[HttpPost("/auth/account/me/changepassword")]
|
||||
[HttpPut("/auth/account/me/changepassword")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public async Task<IActionResult> ChangePassword()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
string? oldPassword = await ReadRequestValueAsync("oldPassword", "old_password");
|
||||
string? newPassword = await ReadRequestValueAsync("newPassword", "new_password", "password");
|
||||
if (string.IsNullOrEmpty(newPassword) ||
|
||||
newPassword.Length < PasswordSecurity.MinPasswordLength ||
|
||||
newPassword.Length > PasswordSecurity.MaxPasswordLength)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "invalid_password",
|
||||
message = "Password must be at least 8 characters."
|
||||
});
|
||||
}
|
||||
|
||||
if (!PlayerDB.SetPassword(id.Value, newPassword, oldPassword))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "invalid_old_password",
|
||||
message = "The old password did not match."
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("account/me/haspassword")]
|
||||
public IActionResult HasPassword()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
return id.HasValue
|
||||
? Ok(PlayerDB.HasPassword(id.Value))
|
||||
: Unauthorized();
|
||||
}
|
||||
|
||||
[HttpGet("account/me/email")]
|
||||
public IActionResult GetMyEmail()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
var email = PlayerDB.GetEmail(id.Value);
|
||||
if (email == null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(new { email });
|
||||
}
|
||||
|
||||
[HttpPost("account/me/email")]
|
||||
[RequestSizeLimit(16 * 1024)]
|
||||
public IActionResult SetMyEmail([FromForm] string email)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
email = email?.Trim() ?? string.Empty;
|
||||
bool validEmail = false;
|
||||
if (email.Length <= 254)
|
||||
{
|
||||
try
|
||||
{
|
||||
validEmail = string.Equals(
|
||||
new System.Net.Mail.MailAddress(email).Address,
|
||||
email,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (FormatException) { }
|
||||
}
|
||||
|
||||
if (!validEmail)
|
||||
return BadRequest();
|
||||
|
||||
if (!PlayerDB.UpdateEmail(id.Value, email))
|
||||
return BadRequest();
|
||||
|
||||
Console.WriteLine($"[EMAIL] Player {id.Value} updated their email address.");
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("account/me")]
|
||||
public IActionResult GetAccountMe()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
if (id == null)
|
||||
return Unauthorized();
|
||||
|
||||
var account = PlayerDB.GetAccountMe(id.Value);
|
||||
|
||||
if (account == null)
|
||||
return NotFound();
|
||||
|
||||
Console.WriteLine($"[ACCOUNT ME] Auth ID: {id.Value}");
|
||||
return Ok(account);
|
||||
}
|
||||
|
||||
[HttpPut("account/me/personalpronouns")]
|
||||
[RequestSizeLimit(8 * 1024)]
|
||||
public async Task<IActionResult> UpdatePersonalPronouns()
|
||||
{
|
||||
long? id = AuthStuff.GetPlayerId(Request);
|
||||
if (!id.HasValue)
|
||||
return Unauthorized();
|
||||
|
||||
string? raw = await ReadRequestValueAsync(
|
||||
"personalPronouns",
|
||||
"PersonalPronouns",
|
||||
"personalPronoun",
|
||||
"PersonalPronoun",
|
||||
"personal_pronouns",
|
||||
"pronouns",
|
||||
"pronoun",
|
||||
"value");
|
||||
|
||||
Console.WriteLine(
|
||||
$"[PERSONAL PRONOUNS] account={id.Value} " +
|
||||
$"contentType={Request.ContentType ?? "null"} " +
|
||||
$"raw={raw ?? "null"}");
|
||||
|
||||
if (!TryParsePersonalPronouns(raw, out int personalPronouns))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = "invalid_personal_pronouns",
|
||||
received = raw,
|
||||
contentType = Request.ContentType
|
||||
});
|
||||
}
|
||||
|
||||
if (personalPronouns < 0 || (personalPronouns & ~0x3F) != 0)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = "personal_pronouns_out_of_range",
|
||||
received = personalPronouns,
|
||||
allowedMinimum = 0,
|
||||
allowedMaximum = 63
|
||||
});
|
||||
}
|
||||
|
||||
if (!PlayerDB.UpdatePersonalPronouns(id.Value, personalPronouns))
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
success = false,
|
||||
error = "personal_pronouns_update_failed"
|
||||
});
|
||||
}
|
||||
|
||||
var account = PlayerDB.GetAccountMe(id.Value);
|
||||
|
||||
Console.WriteLine(
|
||||
$"[PERSONAL PRONOUNS] account={id.Value} saved={personalPronouns}");
|
||||
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(id.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
personalPronouns,
|
||||
account
|
||||
});
|
||||
}
|
||||
|
||||
private static bool TryParsePersonalPronouns(
|
||||
string? raw,
|
||||
out int personalPronouns)
|
||||
{
|
||||
personalPronouns = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return false;
|
||||
|
||||
string cleaned = Uri.UnescapeDataString(raw)
|
||||
.Trim()
|
||||
.Trim((char)34, (char)39);
|
||||
|
||||
if (int.TryParse(cleaned, out personalPronouns))
|
||||
return true;
|
||||
|
||||
int equalsIndex = cleaned.LastIndexOf('=');
|
||||
if (equalsIndex >= 0 && equalsIndex < cleaned.Length - 1)
|
||||
{
|
||||
string valuePart = cleaned[(equalsIndex + 1)..]
|
||||
.Trim()
|
||||
.Trim((char)34, (char)39);
|
||||
|
||||
if (int.TryParse(valuePart, out personalPronouns))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[HttpPut("account/me/identityflags")]
|
||||
[RequestSizeLimit(8 * 1024)]
|
||||
public async Task<IActionResult> UpdateIdentityFlags()
|
||||
{
|
||||
long? accountId = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
if (!accountId.HasValue)
|
||||
return Unauthorized();
|
||||
|
||||
string? raw = await ReadRequestValueAsync(
|
||||
"identityFlags",
|
||||
"IdentityFlags",
|
||||
"identityFlag",
|
||||
"flags",
|
||||
"value");
|
||||
|
||||
if (!TryParsePersonalPronouns(raw, out int identityFlags) ||
|
||||
identityFlags < 0)
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
success = false,
|
||||
error = "invalid_identity_flags",
|
||||
received = raw
|
||||
});
|
||||
}
|
||||
|
||||
if (!PlayerDB.UpdateIdentityFlags(
|
||||
accountId.Value,
|
||||
identityFlags))
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
success = false,
|
||||
error = "identity_flags_update_failed"
|
||||
});
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"[IDENTITY FLAGS] account={accountId.Value} saved={identityFlags}");
|
||||
|
||||
await NotiController.NotifyPlayerProfileUpdatedAsync(accountId.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
identityFlags,
|
||||
account = PlayerDB.GetAccountMe(accountId.Value)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("account/search")]
|
||||
public IActionResult SearchAccounts([FromQuery] string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "name_required",
|
||||
message = "A name must be provided."
|
||||
});
|
||||
}
|
||||
|
||||
var authId = AuthStuff.GetPlayerId(Request);
|
||||
string query = name.Trim();
|
||||
|
||||
var matchingAccountIds = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Where(account =>
|
||||
account.Player != null &&
|
||||
(
|
||||
(!string.IsNullOrWhiteSpace(account.Player.Username) &&
|
||||
account.Player.Username.Contains(
|
||||
query,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
))
|
||||
||
|
||||
(!string.IsNullOrWhiteSpace(account.Player.DisplayName) &&
|
||||
account.Player.DisplayName.Contains(
|
||||
query,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
))
|
||||
)
|
||||
)
|
||||
.OrderByDescending(account =>
|
||||
string.Equals(
|
||||
account.Player!.Username,
|
||||
query,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
)
|
||||
.ThenByDescending(account =>
|
||||
string.Equals(
|
||||
account.Player!.DisplayName,
|
||||
query,
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
)
|
||||
)
|
||||
.ThenBy(account => account.Player!.Username)
|
||||
.Take(20)
|
||||
.Select(account => account.PlayerId)
|
||||
.ToList();
|
||||
|
||||
var accounts = PlayerDB.GetAccountsBulk(
|
||||
matchingAccountIds,
|
||||
authId
|
||||
);
|
||||
|
||||
Console.WriteLine(
|
||||
$"[ACCOUNT SEARCH] \"{query}\" returned {matchingAccountIds.Count} account(s)"
|
||||
);
|
||||
|
||||
return Ok(accounts);
|
||||
}
|
||||
|
||||
private async Task<string?> ReadRequestValueAsync(params string[] keys)
|
||||
{
|
||||
foreach (string key in keys)
|
||||
{
|
||||
string? queryValue = Request.Query[key].FirstOrDefault();
|
||||
if (queryValue != null)
|
||||
return queryValue;
|
||||
}
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync(
|
||||
HttpContext.RequestAborted);
|
||||
|
||||
foreach (string key in keys)
|
||||
{
|
||||
string? formValue = form[key].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(formValue))
|
||||
return formValue;
|
||||
}
|
||||
|
||||
if (form.Count == 1)
|
||||
{
|
||||
var onlyField = form.First();
|
||||
string? onlyValue = onlyField.Value.FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(onlyValue))
|
||||
return onlyValue;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(onlyField.Key))
|
||||
return onlyField.Key;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Request.ContentLength == 0)
|
||||
return null;
|
||||
|
||||
string rawBody;
|
||||
using (var reader = new StreamReader(Request.Body))
|
||||
rawBody = await reader.ReadToEndAsync();
|
||||
if (string.IsNullOrWhiteSpace(rawBody))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(rawBody);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return document.RootElement.ValueKind is
|
||||
JsonValueKind.String or JsonValueKind.Number or
|
||||
JsonValueKind.True or JsonValueKind.False
|
||||
? document.RootElement.ToString()
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach (var property in document.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!keys.Any(key => string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
continue;
|
||||
|
||||
return property.Value.ValueKind == JsonValueKind.String
|
||||
? property.Value.GetString()
|
||||
: property.Value.ToString();
|
||||
}
|
||||
|
||||
var onlyProperty = document.RootElement.EnumerateObject().Take(2).ToArray();
|
||||
if (onlyProperty.Length == 1)
|
||||
{
|
||||
return onlyProperty[0].Value.ValueKind == JsonValueKind.String
|
||||
? onlyProperty[0].Value.GetString()
|
||||
: onlyProperty[0].Value.ToString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
|
||||
return rawBody.Trim().Trim('"');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user