using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Security.Cryptography; using DorkNet.Server.Auth; using DorkNet.Server.Data; using DorkNet.Server.Data.Entities; namespace DorkNet.Server.Controllers.API.ItemWishlists; /// /// api.rec.net/api/itemWishlists — consumable/gift-drop wishlist /// (RecNet.Runtime BPAIBFBKKCN). Wire contract verified against the /// 2023.03.21 ISIL decompile: /// /// GET api/itemWishlists/v1/wishlist/{accountId|me} /// - BPAIBFBKKCN.txt:700-763 (ELPBIBPOBJF) — URL is /// String.Concat("api/itemWishlists/v1/wishlist/", idOrMe) where /// idOrMe is "me" for the local account, else the numeric account /// id as a PATH SEGMENT (not a query param). Missing this route /// 404s with an empty body → CHGIJBGGJAG "Response was empty" → /// "Failed to get wishlist for player" (Player.log:2832-2872). /// - Return type FGLDKEJLAKB<List<BFJNGMGONED>> with no converter /// Func → the body is a BARE ARRAY of BFJNGMGONED. /// - BFJNGMGONED = { Guid WishlistItemId, Int32 AccountId, /// Int32 PurchasableItemId, DateTime CreatedAt } /// (property types BFJNGMGONED.txt:3-97; JSON keys registered by /// reader MCDADMNCDEI.txt:343-434, Pascal/camel/lower probes). /// /// PUT api/itemWishlists/v1/wishlist/me/{purchasableItemId} /// - BPAIBFBKKCN.txt:161-215 (OOPPFFPOAFH) — format string /// "{0}/v1/wishlist/me/{1}", verb field = 3 = PUT /// (verb table HNLCIDLIIBO.txt:878-903). Response is parsed as the /// AECMPGPHAII<BFJNGMGONED> envelope {"Value","Success","Error"} /// (envelope reader keys LEEAJGDIOHI.txt:243-286). /// /// DELETE api/itemWishlists/v1/wishlist/me/{purchasableItemId} /// - BPAIBFBKKCN.txt:375-419 (IBLKDCAIODF) — same URL, verb 4 = /// DELETE, fire-and-forget (LDGADANDBIO) — body ignored. /// /// Storage: rows live in ItemWishlists with the purchasable item id /// serialized into the existing ItemKey column (no schema change); /// WishlistItemId is derived deterministically from the row id. /// [ApiController] [Authorize] public class ItemWishlistsController(DorkNetDbContext db) : ControllerBase { private long Me => this.RequireCurrentPlayerId(); [HttpGet("api/itemWishlists/v1/wishlist/me")] public async Task Mine() => await ForPlayer(Me); [HttpGet("api/itemWishlists/v1/wishlist/{accountId:long}")] public async Task ForPlayer(long accountId) { var rows = await db.ItemWishlists .Where(w => w.PlayerId == accountId) .OrderByDescending(w => w.CreatedAt) .ToListAsync(); return Ok(rows.Select(ToWire).Where(w => w is not null).ToArray()); } [HttpPut("api/itemWishlists/v1/wishlist/me/{purchasableItemId:int}")] [HttpPost("api/itemWishlists/v1/wishlist/me/{purchasableItemId:int}")] public async Task Add(int purchasableItemId) { var key = purchasableItemId.ToString(); var row = await db.ItemWishlists.FirstOrDefaultAsync(w => w.PlayerId == Me && w.ItemKey == key); if (row is null) { row = new ItemWishlistEntity { PlayerId = Me, ItemKey = key, ItemType = 0, }; db.ItemWishlists.Add(row); await db.SaveChangesAsync(); } // AECMPGPHAII envelope: {"Value","Success","Error"} // (LEEAJGDIOHI.txt:243-286). return Ok(new { Value = ToWire(row), Success = true, Error = (string?)null, }); } [HttpDelete("api/itemWishlists/v1/wishlist/me/{purchasableItemId:int}")] public async Task Remove(int purchasableItemId) { var key = purchasableItemId.ToString(); await db.ItemWishlists .Where(w => w.PlayerId == Me && w.ItemKey == key) .ExecuteDeleteAsync(); // Fire-and-forget on the client (LDGADANDBIO) — body ignored. return Ok(new { Success = true }); } /// BFJNGMGONED wire item. Rows whose ItemKey isn't a /// purchasable-item integer (legacy free-form keys) are skipped. private static object? ToWire(ItemWishlistEntity row) { if (!int.TryParse(row.ItemKey, out var purchasableItemId)) return null; return new { WishlistItemId = StableGuid(row.Id, row.PlayerId), AccountId = (int)row.PlayerId, PurchasableItemId = purchasableItemId, row.CreatedAt, }; } /// Deterministic per-row Guid — the client only uses /// WishlistItemId as an opaque identity, but it must stay stable /// across requests. private static Guid StableGuid(long rowId, long playerId) { Span input = stackalloc byte[16]; BitConverter.TryWriteBytes(input, rowId); BitConverter.TryWriteBytes(input[8..], playerId); Span hash = stackalloc byte[32]; SHA256.HashData(input, hash); return new Guid(hash[..16]); } }